diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml new file mode 100644 index 0000000000..27ec1b2828 --- /dev/null +++ b/.github/workflows/coding-style.yml @@ -0,0 +1,17 @@ +name: Coding Style + +on: [push, pull_request] + +jobs: + nette_cc: + name: Nette Code Checker + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + coverage: none + + - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress + - run: php temp/code-checker/code-checker --no-progress diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 550b89053d..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: php - -script: - - php code-checker/src/code-checker.php - -before_script: - - travis_retry composer create-project nette/code-checker code-checker ~2.5 --no-interaction - -sudo: false - -cache: - directories: - - $HOME/.composer/cache diff --git a/application/bg/@home.texy b/application/bg/@home.texy index e01b54f4ee..200b466cc7 100644 --- a/application/bg/@home.texy +++ b/application/bg/@home.texy @@ -1,36 +1,85 @@ -Приложение Nette -**************** +Заявление за Nette +****************** .[perex] -Пакетът `nette/application` е основа за създаване на интерактивни уеб приложения. +Nette Application е ядрото на рамката Nette, която предоставя мощни инструменти за създаване на модерни уеб приложения. Тя предлага множество изключителни функции, които значително опростяват разработката и подобряват сигурността и поддържането на кода. -- [Как работят приложенията |how-it-works]? -- [Bootstrap |Bootstrap] -- [Презентатори |presenters] -- [Шаблони |templates] -- [Модули |modules] -- [Маршрутизиране |routing] -- [Създаване на URL |creating-links] -- [Интерактивни компоненти |components] -- [AJAX и фрагменти |ajax] -- [Мултипликатор |Multiplier] -- [Конфигурация |configuration] +Инсталация .[#toc-installation] +------------------------------- -Настройка ---------- - -Изтеглете и инсталирайте пакета с помощта на [Composer |best-practices:composer]: +Изтеглете и инсталирайте библиотеката, като използвате [Composer |best-practices:composer]: ```shell composer require nette/application ``` -| версия на пакета | съвместима версия на PHP -|-----------------------|----------------------- -| Nette Application 4.0 | PHP 8.0 - 8.1 -| Nette Application 3.1 | PHP 7.2 - 8.1 + +Защо да изберете Nette Application? .[#toc-why-choose-nette-application] +------------------------------------------------------------------------ + +Nette винаги е била пионер в областта на уеб технологиите. + +**Двупосочен маршрутизатор:** Nette разполага с усъвършенствана система за маршрутизация, уникална със своята двупосочност - тя не само превежда URL адреси към действия на приложението, но може да генерира и URL адреси в обратна посока. Това означава: +- Можете да променяте структурата на URL адресите на цялото приложение по всяко време, без да променяте файловете на шаблоните. +- URL адресите се канонизират автоматично, което подобрява SEO +- Маршрутизацията се определя на едно място, а не е разпръсната в анотации + +**Компоненти и сигнали:** Вградената система от компоненти, вдъхновена от Delphi и React.js, е уникална сред PHP фреймуърците: +- Позволява създаването на елементи на потребителския интерфейс за многократна употреба +- Поддържа йерархично композиране на компоненти +- Предлага елегантна обработка на AJAX заявки чрез сигнали +- Богата библиотека от готови компоненти в [Componette](https://componette.org) + +**AJAX и Snippets:** Nette представи революционен начин за работа с AJAX през 2009 г., преди решения като Hotwire за Ruby on Rails или Symfony UX Turbo: +- Извадките позволяват актуализиране само на части от страницата, без да се пише JavaScript +- Автоматична интеграция със системата от компоненти +- Интелигентно обезсилване на части от страницата +- Минимално прехвърляне на данни + +**Интуитивни шаблони [Latte |latte:]:** Най-сигурната система за шаблониране за PHP с разширени функции: +- Автоматична защита от XSS с контекстно ескапиране +- Възможност за разширяване с персонализирани филтри, функции и тагове +- Наследяване на шаблони и фрагменти за AJAX +- Отлична поддръжка на PHP 8.x със система от типове + +**Вкачване на зависимости:** Nette използва напълно функцията "Вкачване на зависимости" (Dependency Injection): +- Автоматично предаване на зависимости (autowiring) +- Конфигуриране чрез ясен формат NEON +- Поддръжка на фабрики за компоненти + + +Основни предимства .[#toc-main-benefits] +---------------------------------------- + +- **Сигурност**: Автоматична защита срещу [уязвимости |nette:vulnerability-protection] като XSS, CSRF и др. +- **Продуктивност**: По-малко писане, повече функции благодарение на интелигентния дизайн +- **Отстраняване на грешки**: [Дебъгер на Tracy с |tracy:] панел за маршрутизация +- **Производителност**: Интелигентна система за кеширане, лениво зареждане на компоненти +- **Гъвкавост**: Лесна промяна на URL адреса дори след завършване на приложението +- **Компоненти**: Уникална система от елементи на потребителския интерфейс за многократна употреба +- **Модерна**: Пълна поддръжка на PHP 8.4+ и система от типове + + +Започване на работа .[#toc-getting-started] +------------------------------------------- + +1. [Разбиране на приложенията |how-it-works] - Разбиране на основната архитектура +2. [Презентатори |presenters] - Работа с презентатори и действия +3. [Шаблони |templates] - Създаване на шаблони в Latte +4. [Маршрутизиране |routing] - Конфигуриране на URL +5. [Интерактивни компоненти |components] - използване на системата от компоненти + + +Съвместимост с PHP .[#toc-php-compatibility] +-------------------------------------------- + +| версия | съвместима с 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 -Отнася се за най-новите версии на кръпките. +Валидно за последните версии на кръпките. \ No newline at end of file diff --git a/application/bg/@left-menu.texy b/application/bg/@left-menu.texy index 24596b3137..454f611fb0 100644 --- a/application/bg/@left-menu.texy +++ b/application/bg/@left-menu.texy @@ -4,7 +4,7 @@ - [Bootstrap |Bootstrap] - [Презентатори |presenters] - [Шаблони |templates] -- [Модули |modules] +- [Структура на директорията |directory-structure] - [Маршрутизиране |routing] - [Създаване на URL |creating-links] - [Интерактивни компоненти |components] diff --git a/application/bg/ajax.texy b/application/bg/ajax.texy index 1bcf75bc98..04cc36448d 100644 --- a/application/bg/ajax.texy +++ b/application/bg/ajax.texy @@ -3,10 +3,10 @@ AJAX и фрагменти
-Съвременните уеб приложения днес се изпълняват наполовина на сървъра и наполовина в браузъра. AJAX е важен обединяващ фактор. Каква поддръжка предлага рамката Nette? -- Подаване на фрагменти (наречени *фрагменти*) -- прехвърляне на променливи между PHP и JavaScript -- отстраняване на грешки в приложенията AJAX +В ерата на съвременните уеб приложения, където функционалността често се разпростира между сървъра и браузъра, AJAX е важен свързващ елемент. Какви възможности предлага Nette Framework в тази област? +- Изпращане на части от шаблона, т.нар. фрагменти +- предаване на променливи между PHP и JavaScript +- инструменти за отстраняване на грешки при AJAX заявките
@@ -14,29 +14,32 @@ AJAX и фрагменти Заявка AJAX .[#toc-ajax-request] ================================ -Заявката AJAX не се различава от класическата заявка - водещият се извиква с определен изглед и параметри. От водещия също зависи как да отговори на нея: той може да използва своя собствена процедура, която връща фрагмент от HTML код (HTML snippet), XML документ, JSON обект или JavaScript код. +Заявката AJAX не се различава съществено от класическата HTTP заявка. Извиква се презентатор с определени параметри. От водещия зависи как да отговори на заявката - той може да върне данни във формат JSON, да изпрати част от HTML код, XML документ и т.н. -От страна на сървъра AJAX заявката може да бъде открита с помощта на метода на услугата, [капсулиращ HTTP заявката |http:request] `$httpRequest->isAjax()` (открива се въз основа на HTTP заглавието `X-Requested-With`). Вътре в презентатора е наличен пряк път под формата на метода `$this->isAjax()`. +От страна на браузъра инициираме AJAX заявка, като използваме функцията `fetch()`: -Съществува предварително обработен обект `payload`, предназначен за изпращане на данни към браузъра във формат JSON. - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Успешно'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // обработка на отговора +}); ``` -За да имате пълен контрол върху извеждането на JSON, използвайте метода `sendJson` в презентатора. Това веднага ще прекъсне презентатора и ще се справите без шаблона: +От страна на сървъра AJAX заявката се разпознава чрез метода `$httpRequest->isAjax()` на услугата, която [капсулира HTTP заявката |http:request]. Той използва HTTP заглавието `X-Requested-With`, така че е от съществено значение да го изпратите. В рамките на презентатора можете да използвате метода `$this->isAjax()`. + +Ако искате да изпратите данни във формат JSON, използвайте метода [`sendJson()` |presenters#Sending a response] метод. Методът също така прекратява дейността на презентатора. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Ако искаме да изпращаме HTML, можем да създадем специален шаблон за AJAX заявки: +Ако планирате да отговорите със специален шаблон, предназначен за AJAX, можете да го направите по следния начин: ```php public function handleClick($param): void @@ -44,27 +47,43 @@ public function handleClick($param): void if ($this->isAjax()) { $this->template->setFile('path/to/ajax.latte'); } - // ... + //... } ``` +Извадки .[#toc-snippets] +======================== + +Най-мощният инструмент, предлаган от Nette за свързване на сървъра с клиента, са фрагментите. С тях можете да превърнете едно обикновено приложение в AJAX приложение с минимални усилия и няколко реда код. Примерът Fifteen демонстрира как работи всичко това, а кодът му може да бъде намерен в [GitHub |https://github.com/nette-examples/fifteen]. + +Извадките или изрезките ви позволяват да актуализирате само части от страницата, вместо да презареждате цялата страница. Това е по-бързо и по-ефективно, а също така осигурява по-удобно потребителско изживяване. Snippets може да ви напомнят за Hotwire за Ruby on Rails или Symfony UX Turbo. Интересното е, че Nette въвежда фрагментите 14 години по-рано. + +Как работят отрязъците? Когато страницата се зарежда за първи път (заявка, която не е свързана с AJAX), се зарежда цялата страница, включително всички фрагменти. Когато потребителят взаимодейства със страницата (напр. щракне върху бутон, изпрати формуляр и т.н.), вместо да се зареди цялата страница, се прави AJAX заявка. Кодът в презентатора извършва действието и решава кои фрагменти се нуждаят от актуализиране. Nette визуализира тези фрагменти и ги изпраща под формата на JSON масив. След това кодът за обработка в браузъра вмъква получените фрагменти обратно в страницата. Следователно се прехвърля само кодът на променените фрагменти, което спестява честотна лента и ускорява зареждането в сравнение с прехвърлянето на цялото съдържание на страницата. + + Naja .[#toc-naja] -================= +----------------- -[Библиотеката Naja |https://naja.js.org] се използва за обработка на AJAX заявки от страна на браузъра. [Инсталирайте |https://naja.js.org/#/guide/01-install-setup-naja] го като пакет за node.js (за използване с Webpack, Rollup, Vite, Parcel и други): +За обработка на фрагменти от страна на браузъра се използва [библиотеката Naja |https://naja.js.org]. [Инсталирайте я |https://naja.js.org/#/guide/01-install-setup-naja] като пакет за node.js (за използване с приложения като Webpack, Rollup, Vite, Parcel и други): ```shell npm install naja ``` -...или да вмъкнете директно в шаблон на страница: +... или я вмъкнете директно в шаблона на страницата: ```html ``` -За да създадете AJAX заявка от обикновена връзка (сигнал) или подаване на формуляр, просто маркирайте съответната връзка, формуляр или бутон с класа `ajax`: +Първо трябва да [инициализирате |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] библиотеката: + +```js +naja.initialize(); +``` + +За да превърнете обикновена връзка (сигнал) или подаване на форма в AJAX заявка, просто маркирайте съответната връзка, форма или бутон с класа `ajax`: ```html Go @@ -74,64 +93,39 @@ npm install naja or +
``` -Извадки .[#toc-snippety] -======================== - -Има обаче много по-мощен инструмент за вградена поддръжка на AJAX - фрагменти. Използването им ви позволява да превърнете обикновено приложение в AJAX приложение само с няколко реда код. Как работи всичко това, е показано в примера Fifteen, чийто код също е наличен в компилацията или в [GitHub |https://github.com/nette-examples/fifteen]. - -Принципът на фрагментите е, че цялата страница се прехвърля по време на първоначалната (т.е. не-AJAX) заявка и след това при всяка AJAX [подзаявка |components#Signal] (заявка на същия изглед от същия водещ) само кодът на променените части се прехвърля в хранилището `payload`, споменато по-рано. - -Извадките може да ви напомнят за Hotwire за Ruby on Rails или Symfony UX Turbo, но Nette ги изобретява четиринадесет години по-рано. - +Прерисуване на фрагменти .[#toc-redrawing-snippets] +--------------------------------------------------- -Инвалидизация .[#toc-invalidation-of-snippets] -============================================== - -Всеки наследник на класа [Control |components] (какъвто е Presenter) може да запомни дали по време на заявката е имало промени, които изискват повторно картографиране. Съществуват няколко начина за справяне с това: `redrawControl()` и `isControlInvalid()`. Пример: +Всеки обект от класа [Control |components] (включително самият Presenter) запазва запис дали са настъпили промени, които налагат прерисуването му. За тази цел се използва методът `redrawControl()`. ```php public function handleLogin(string $user): void { - // Обектът трябва да бъде показан отново, след като потребителят е влязъл в системата + // след като влезете в системата, е необходимо да прерисувате съответната част $this->redrawControl(); - // ... + //... } ``` -Nette обаче осигурява още по-фина разделителна способност от целите компоненти. Изброените методи приемат името на така наречения "фрагмент" като незадължителен параметър. "Фрагмент" по същество е елемент от вашия шаблон, маркиран с макрос Latte за тази цел, за който ще стане дума по-късно. По този начин можете да поискате от компонент да прерисува само *част* от шаблона. Ако целият компонент е невалиден, всички негови фрагменти се прерисуват. Компонент е "невалиден", ако някой от неговите подкомпоненти е невалиден. - -```php -$this->isControlInvalid(); // -> false -$this->redrawControl('header'); // обезсилване на фрагмента с име 'header' -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, поне един фрагмент е невалиден +Nette също така позволява по-фино управление на това, което трябва да се прерисува. Гореспоменатият метод може да приема името на фрагмента като аргумент. По този начин е възможно да се обезсили (което означава: да се наложи прерисуване) на ниво част от шаблона. Ако целият компонент бъде обезсилен, всеки фрагмент от него също ще бъде прерисуван: -$this->redrawControl(); // обезсилва целия компонент, всеки фрагмент -$this->isControlInvalid('footer'); // -> true +```php +// обезсилва фрагмента 'header' +$this->redrawControl('header'); ``` -Компонент, който е получил сигнал, автоматично се маркира за прерисуване. - -Чрез преначертаване на срезовете знаем точно кои части от кои елементи трябва да бъдат преначертани. - - -Етикет `{snippet} … {/snippet}` .{toc: Tag snippet} -=================================================== - -Страницата се визуализира по същия начин, както при обикновена заявка: зареждат се същите шаблони и т.н. Най-важното обаче е да се предотвратят онези части, които не трябва да се извеждат; останалите части трябва да се свържат с идентификатор и да се изпратят на потребителя във формат, който обработващият JavaScript може да разбере. - -Синтаксис .[#toc-sintaksis] ---------------------------- +Извадки в Latte .[#toc-snippets-in-latte] +----------------------------------------- -Ако в шаблона има контролен елемент или фрагмент, трябва да го обгърнем с помощта на сдвоения таг `{snippet} ... {/snippet}` - визуализираният фрагмент ще бъде "изрязан" и изпратен на браузъра. Освен това той ще го обгърне със спомагателен таг `
` (можете да използвате друга). Следващият пример дефинира фрагмент с име `header`. Той може да представлява и шаблон на компонент: +Използването на фрагменти в Latte е изключително лесно. За да определите част от шаблона като фрагмент, просто я обвийте в тагове `{snippet}` и `{/snippet}`: ```latte {snippet header} @@ -139,7 +133,9 @@ $this->isControlInvalid('footer'); // -> true {/snippet} ``` -Ако искате да създадете фрагмент със съдържащ елемент, различен от `
`, или да добавите потребителски атрибути към елемента, можете да използвате следното определение: +Извадката създава елемент `
` в HTML страницата със специално генериран `id`. При прерисуване на фрагмент съдържанието на този елемент се актуализира. Следователно при първоначалното визуализиране на страницата трябва да се визуализират и всички фрагменти, дори ако първоначално те могат да бъдат празни. + +Можете също така да създадете фрагмент с елемент, различен от `
` като използвате атрибут n:: ```latte
@@ -148,138 +144,106 @@ $this->isControlInvalid('footer'); // -> true ``` -Динамични фрагменти .[#toc-dinamiceskie-snippety] -================================================= +Области на извадките .[#toc-snippet-areas] +------------------------------------------ -В Nette можете също така да дефинирате фрагменти с динамично име въз основа на параметър по време на изпълнение. Това е най-подходящо за различни списъци, в които трябва да променим само един ред, но не искаме да преместим целия списък заедно с него. Пример за това е: +Имената на фрагментите могат да бъдат и изрази: ```latte - +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Съществува един статичен фрагмент `itemsContainer`, който съдържа няколко динамични фрагмента: `пункт-0`, `пункт-1` и т.н. +По този начин ще получим няколко фрагмента като `item-0`, `item-1` и т.н. Ако директно обезсилим динамичен фрагмент (например `item-1`), нищо няма да бъде прерисувано. Причината е, че фрагментите функционират като истински откъси и само те самите се визуализират директно. В шаблона обаче технически няма фрагмент с име `item-1`. Той се появява само при изпълнение на заобикалящия го код на фрагмента, в този случай цикъла foreach. Следователно ще маркираме частта от шаблона, която трябва да се изпълни, с тага `{snippetArea}`: -Не можете да прерисувате динамичния фрагмент директно (прерисуването на `item-1` няма ефект), а трябва да прерисувате родителския фрагмент ( `itemsContainer` в този пример). При това се изпълнява кодът на родителския фрагмент, но на браузъра се предават само неговите вложени фрагменти. Ако искате да предадете само един от вложените фрагменти, трябва да промените входа за родителския фрагмент, за да избегнете генерирането на други вложени фрагменти. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -В горния пример трябва да се уверите, че само един елемент ще бъде добавен към масива `$list`, когато бъде направена заявката AJAX, така че цикълът `foreach` ще изведе само един динамичен фрагмент. +И ще прерисуваме както отделния фрагмент, така и цялата обща област: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * Этот метод возвращает данные для списка. - * Обычно это просто запрос данных из модели. - * Для целей этого примера данные жёстко закодированы. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +Също така е важно да се гарантира, че масивът `$items` съдържа само елементите, които трябва да бъдат прерисувани. -Извадки в активирания шаблон .[#toc-snippety-vo-vklyucennom-sablone] -==================================================================== - -Възможно е даден фрагмент да се намира в шаблон, който е включен от друг шаблон. В този случай е необходимо да се обгърне кодът за разрешаване във втория шаблон с макроса `snippetArea`, след което да се прерисуват както областта snippetArea, така и самият фрагмент. - -Макросът `snippetArea` гарантира, че кодът в него ще бъде изпълнен, но само действителният фрагмент от включения шаблон ще бъде изпратен на браузъра. +При вмъкване на друг шаблон в основния с помощта на тага `{include}`, който има фрагменти, е необходимо отново да се обвие включеният шаблон в `snippetArea` и да се обезсилят заедно и фрагментът, и областта: ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* included.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -Можете също така да го комбинирате с динамични фрагменти. +Извадки в компонентите .[#toc-snippets-in-components] +----------------------------------------------------- -Добавяне и премахване .[#toc-dobavlenie-i-udalenie] -=================================================== - -Ако добавите нов елемент в списъка и отмените `itemsContainer`, заявката AJAX ще върне фрагменти, включващи новия елемент, но обработващият javascript няма да може да го визуализира. Това е така, защото няма HTML елемент с новосъздадения ID. - -В този случай най-лесният начин е да обвиете целия списък в друг фрагмент и да го обезсилите: +Можете да създавате фрагменти в [компонентите |components] и Nette автоматично ще ги прерисува. Има обаче специфично ограничение: за да прерисува отрязъци, той извиква метода `render()` без никакви параметри. По този начин подаването на параметри в шаблона няма да работи: ```latte -{snippet wholeList} - -{/snippet} -Добавить +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Изпращане на потребителски данни .[#toc-sending-user-data] +---------------------------------------------------------- + +Заедно с фрагментите можете да изпращате всякакви допълнителни данни на клиента. Просто ги запишете в обекта `payload`: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + //... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -Същото важи и за изтриване на елемент. Може да се подаде празен фрагмент, но обикновено списъците могат да бъдат странирани и би било трудно да се приложи премахването на един елемент и зареждането на друг (който преди това е бил на друга страница на странирания списък). +Изпращане на параметри .[#toc-sending-parameters] +================================================= -Изпращане на параметри към компонент .[#toc-otpravka-parametrov-komponentu] -=========================================================================== - -Когато изпращаме параметри към компонент чрез заявка AJAX, независимо дали става въпрос за сигнални или постоянни параметри, трябва да предоставим тяхното глобално име, което съдържа и името на компонента. Пълното име на параметъра се връща от метода `getParameterId()`. +Когато изпращаме параметри към компонента чрез AJAX заявка, независимо дали става въпрос за сигнални или постоянни параметри, трябва да предоставим тяхното глобално име, което съдържа и името на компонента. Пълното име на параметъра се връща от метода `getParameterId()`. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -И обработете метода със съответните параметри в компонента. +Метод за обработка със съответните параметри в компонента: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/bg/bootstrap.texy b/application/bg/bootstrap.texy index 8d0b3dc5c4..10bbc07ab5 100644 --- a/application/bg/bootstrap.texy +++ b/application/bg/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + 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 { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // Nette е интелигентен и режимът за разработка се включва автоматично, + // или можете да го включите за определен IP адрес, като разкоментирате следния ред: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Активира Tracy: най-добрият инструмент за отстраняване на грешки "швейцарско ножче". + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: автоматично зарежда всички класове в дадената директория + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Зареждане на конфигурационни файлове + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php .[#toc-index-php] =========================== -В случая на уеб приложения началният файл е `index.php`, който се намира в публичната директория `www/`. Той позволява на класа `Bootstrap` да инициализира средата и връща `$configurator`, който създава контейнера DI. След това тя извлича услугата `Application`, която стартира уеб приложението: +В случая на уеб приложенията основният файл е `index.php`, който се намира в [публичната директория |directory-structure#public-directory-www] `www/`. Това ще накара класа Bootstrap да инициализира средата и да създаде контейнер DI. След това той получава от него услугата `Application`, която стартира уеб приложението: ```php -// инициализиране на средата + получаване на обект Configurator -$configurator = App\Bootstrap::boot(); -// създаване на DI-контейнер -$container = $configurator->createContainer(); -// DI-контейнерът ще създаде обект Nette\Application\Application +$bootstrap = new App\Bootstrap; +// Иницииране на средата + създаване на контейнер DI +$container = $bootstrap->bootWebApplication(); +// Контейнерът DI създава обект Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -//стартиране на приложението Nette +// Стартирайте приложението Nette и обработете входящата заявка $application->run(); ``` @@ -59,26 +84,42 @@ $application->run(); Режим на разработка и производствен режим .[#toc-development-vs-production-mode] ================================================================================ -Nette прави разграничение между два основни режима, в които се изпълнява заявката: разработка и производство. Режимът за разработка е насочен към максимално удобство за програмиста, показва се Tracy, кешът се обновява автоматично при промяна на шаблоните или конфигурацията на DI контейнера и т.н. Производственият режим е ориентиран към производителността, Tracy регистрира само грешки, а промените в шаблоните и другите файлове не се проверяват. +Nette се държи по различен начин в зависимост от това дали работи на сървър за разработка или на производствен сървър: + +🛠️ Режим на разработка: + - Показва лентата за отстраняване на грешки на Tracy с полезна информация (напр. SQL заявки, време за изпълнение, използване на памет). + - Показва подробна страница за грешки с проследяване на извикването на функциите и съдържанието на променливите, когато възникне грешка. + - Автоматично опреснява кеша, когато се променят шаблони на Latte, конфигурационни файлове и др. + + +🚀 Производствен режим: + - Не показва никаква информация за отстраняване на грешки; всички грешки се записват в дневника. + - Показва `ErrorPresenter` или обща страница "Server Error" (Грешка на сървъра), когато възникне грешка. + - Кешът никога не се опреснява автоматично! + - Оптимизиран за скорост и сигурност. -Режимът се избира чрез автоматично разпознаване, така че обикновено не е необходимо да конфигурирате или превключвате нещо ръчно. Режимът за разработка се използва, ако приложението се изпълнява на локален хост (т.е. IP адресът е `127.0.0.1` или `::1`) и няма прокси сървър (т.е. HTTP заглавието му). В противен случай приложението работи в производствен режим. + +Режимът се определя автоматично, така че в повечето случаи не е необходимо да го конфигурирате или превключвате ръчно: + +- Режим на разработка: (IP адрес `127.0.0.1` или `::1`), освен ако не се използва прокси сървър (т.е. въз основа на неговите HTTP заглавия). +- Производствен режим: Активен навсякъде другаде. Ако искате да активирате режима за разработка в други случаи, например за програмисти, които имат достъп от определен IP адрес, можете да използвате `setDebugMode()`: ```php -$configurator->setDebugMode('23.75.345.200'); // един или повече IP адреси +$this->configurator->setDebugMode('23.75.345.200'); // един или повече IP адреси ``` Определено препоръчваме да комбинирате IP адреса с "бисквитка". Ще съхраним тайния токен в "бисквитката" `nette-debug', например, `secret1234`, а режимът за разработка ще бъде активиран за програмистите с тази комбинация от IP и "бисквитка". ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` Можете да деактивирате напълно режима за разработчици, дори за localhost: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` Обърнете внимание, че стойността `true` активира плътно режима за разработчици, което никога не трябва да се случва на производствен сървър. @@ -90,7 +131,7 @@ $configurator->setDebugMode(false); За да улесним дебъгването, ще включим чудесния инструмент [Tracy |tracy:]. В режим за разработчици той визуализира грешките, а в производствен режим записва грешките в определена директория: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ $configurator->enableTracy($appDir . '/log'); Nette използва кеш за DI-контейнер, RobotLoader, шаблони и др. Затова е необходимо да се зададе пътят до директорията, в която се съхранява кешът: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` В Linux или macOS задайте [разрешения за запис |nette:troubleshooting#Setting-Directory-Permissions] за директориите `log/` и `temp/`. @@ -112,7 +153,7 @@ RobotLoader .[#toc-robotloader] Обикновено искаме да заредим класовете автоматично с помощта на [RobotLoader |robot-loader:], така че трябва да го стартираме и да му позволим да зареди класовете от директорията, в която се намира `Bootstrap.php` (т.е. `__DIR__`) и всички негови поддиректории: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ $configurator->createRobotLoader() Конфигураторът ви позволява да зададете часовата зона за вашето приложение. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ $configurator->setTimeZone('Europe/Prague'); Файловете за конфигурация се зареждат с помощта на `addConfig()`: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` Методът `addConfig()` може да се извика няколко пъти, за да се добавят няколко файла. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ if (PHP_SAPI === 'cli') { Параметрите, използвани в конфигурационните файлове, могат да бъдат дефинирани [в раздела `parameters` |dependency-injection:configuration#parameters] и да бъдат взети (или презаписани) от метода `addStaticParameters()` (той има псевдоним `addParameters()`). Важно е, че различните стойности на параметрите водят до генериране на допълнителни DI-контейнери, т.е. допълнителни класове. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ $configurator->addStaticParameters([ Възможно е също така да се добавят динамични параметри към контейнер. Различните им стойности, за разлика от статичните параметри, не генерират нови контейнери DI. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ Достъпът до променливите на средата е лесен с помощта на динамични параметри. Достъпът до тях се осъществява чрез `%env.variable%` в конфигурационните файлове. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ $configurator->addDynamicParameters([ - `%wwwDir%` е абсолютният път до директорията, съдържаща входния файл `index.php` - `%tempDir%` е абсолютният път до директорията за временни файлове - `%vendorDir%` е абсолютният път до директорията, в която Composer инсталира библиотеки +- `%rootDir%` е абсолютният път до главната директория на проекта - `%debugMode%` показва дали приложението е в режим на отстраняване на грешки - `%consoleMode%` показва дали заявката е постъпила от командния ред @@ -225,7 +268,7 @@ services: Създайте нов екземпляр и го вмъкнете в Bootstrap: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Различни среди .[#toc-different-environments] ============================================= -Не се колебайте да персонализирате класа `Bootstrap` според нуждите си. Можете да добавите параметри към метода `boot()`, за да разделите уеб проектите, или да добавите други методи, като например `bootForTests()`, който инициализира средата за тестове на единици, `bootForCli()` за скриптове, извикани от командния ред, и т.н. +Не се колебайте да персонализирате класа `Bootstrap` според нуждите си. Можете да добавите параметри към метода `bootWebApplication()`, за да разграничите отделните уеб проекти. Като алтернатива можете да добавите и други методи, например `bootTestEnvironment()` за инициализиране на средата за unit тестове, `bootConsoleApplication()` за скриптове, извикани от командния ред, и т.н. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container +{ + Tester\Environment::setup(); // Инициализация на Nette Tester + $this->setupContainer(); + return $this->configurator->createContainer(); +} + +public function bootConsoleApplication(): Nette\DI\Container { - $configurator = self::boot(); - Tester\Environment::setup(); // Инициализация Nette Tester - return $configurator; + $this->configurator->setDebugMode(false); + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); } ``` diff --git a/application/bg/components.texy b/application/bg/components.texy index 3bd9a2ebf9..ab5c216e1c 100644 --- a/application/bg/components.texy +++ b/application/bg/components.texy @@ -230,6 +230,28 @@ $this->redirect(/* ... */); // пренасочване ``` +Пренасочване след сигнал .[#toc-redirection-after-a-signal] +=========================================================== + +След обработката на сигнал от компонент често следва пренасочване. Тази ситуация е подобна на формулярите - след изпращане на формуляр също пренасочваме, за да предотвратим повторното изпращане на данни, когато страницата се опреснява в браузъра. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Тъй като компонентът е елемент за многократна употреба и обикновено не трябва да има пряка зависимост от конкретни презентатори, методите `redirect()` и `link()` автоматично интерпретират параметъра като сигнал за компонент: + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +Ако трябва да пренасочите към друг презентатор или действие, можете да го направите чрез презентатора: + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Постоянни параметри .[#toc-persistent-parameters] ================================================= @@ -347,7 +369,7 @@ services: Накрая ще използваме тази фабрика в нашия презентатор: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ interface PollControlFactory Компонентите в Nette Application са части за многократна употреба на уеб приложение, които вграждаме в страници, което е предмет на тази глава. Какви са възможностите на такъв компонент? 1) може да се показва в шаблон -2) той знае каква част от себе си да покаже по време на [заявката AJAX |ajax#invalidation] (фрагменти) +2) знае [коя част от себе си |ajax#snippets] да визуализира по време на заявка AJAX (фрагменти). 3) има възможност да съхранява състоянието си в URL (постоянни параметри). 4) има възможност да реагира на действията (сигналите) на потребителя. 5) създава йерархична структура (където коренът е главният). diff --git a/application/bg/configuration.texy b/application/bg/configuration.texy index c017613f9c..0456554386 100644 --- a/application/bg/configuration.texy +++ b/application/bg/configuration.texy @@ -13,11 +13,15 @@ application: # показва раздела "Nette Application" на синия екран на Tracy? debugger: ... # (bool) по подразбиране е true - # ще бъде ли извикан презентаторът на грешки в случай на грешка? - catchExceptions: ... # (bool) по подразбиране е true на "battle" сървър + # Ще се извика ли error-presenter при грешка? + # има ефект само в режим за разработчици + catchExceptions: ... # (bool) по подразбиране е true # име на водещ на грешка - errorPresenter: Error # (string) по подразбиране е 'Nette:Error' + errorPresenter: Error # (string|array) по подразбиране е 'Nette:Error' + + # дефинира псевдоними за водещи и събития + aliases: ... # дефинира правила за съпоставяне на името на водещия с клас mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) по подразбиране е false ``` -Тъй като в режим на разработка презентаторите на грешки не се извикват по подразбиране, а грешките се показват от Tracy, промяната на стойността на `catchExceptions` на `true` помага да се провери дали презентаторите на грешки работят правилно по време на разработката. +От версия 3.2 на `nette/application` е възможно да се дефинира двойка представящи грешки: + +```neon +application: + errorPresenter: + 4xx: Error4xx # за Nette\Application\BadRequestException + 5xx: Error5xx # за други изключения +``` Опцията `silentLinks` определя как Nette да се държи в режим на разработчик, когато генерирането на връзки е неуспешно (например поради липса на презентатор и т.н.). Стойността по подразбиране `false` означава, че Nette работи `E_USER_WARNING`. Задаването на `true` потиска това съобщение за грешка. В производствена среда винаги се извиква `E_USER_WARNING`. Можем също така да повлияем на това поведение, като зададем променливата на водещия [$invalidLinkMode |creating-links#Invalid-Links]. -Съпоставянето [определя правилата, по които |modules#Mapping] името на класа се извежда от основното име. +[Псевдонимите опростяват препратките към |creating-links#aliases] често използвани презентатори. + +[Съпоставянето определя правилата, по които |directory-structure#Presenter Mapping] името на класа се получава от името на водещия. Автоматично регистриране на водещи .[#toc-automatic-registration-of-presenters] @@ -82,6 +95,9 @@ latte: # позволява [проверка на генерирания код |latte:develop#Checking Generated Code] phpLinter: ... # (string) по подразбиране е null + # задава локала + locale: cs_CZ # (string) по подразбиране е null + # клас $this->template templateClass: App\MyTemplateClass # по подразбиране Nette\Bridges\ApplicationLatte\DefaultTemplate ``` @@ -91,7 +107,7 @@ latte: ```neon latte: расширения: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` /--comment diff --git a/application/bg/creating-links.texy b/application/bg/creating-links.texy index 659326e6fe..5809ef5a22 100644 --- a/application/bg/creating-links.texy +++ b/application/bg/creating-links.texy @@ -38,7 +38,7 @@ подробнее ``` -Ако методът `ProductPresenter::renderShow()` няма `$lang` в сигнатурата си, той може да прочете стойността на параметъра, като използва `$lang = $this->getParameter('lang')`. +Ако методът `ProductPresenter::renderShow()` няма `$lang` в сигнатурата си, той може да извлече стойността на параметъра, като използва `$lang = $this->getParameter('lang')` или от [свойството |presenters#Request Parameters]. Ако параметрите се съхраняват в масив, те могат да бъдат разширени с помощта на оператора `(expand)` (нещо като `...` в PHP, но работи с асоциативни масиви): @@ -103,7 +103,7 @@ $url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); главная страница ``` -Връзките могат да сочат и към други [модули |modules]. Тук връзките се разграничават на относителни към подмодули или абсолютни. Принципът е подобен на дисковите пътища, само че с двоеточия вместо с наклонени черти. Да предположим, че водещият е част от модул `Front`, тогава записваме: +Връзките могат да сочат и към други [модули |directory-structure#Presenters and Templates]. Тук връзките се разграничават на относителни към подмодулите или абсолютни. Принципът е аналогичен на дисковите пътища, само че вместо наклонена черта има двоеточия. Нека приемем, че действителният презентатор е част от модула `Front`, тогава ще напишем: ```latte ссылка на Front:Shop:Product:show @@ -140,7 +140,7 @@ $url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); обновить ``` -Това ще предаде всички параметри, посочени в сигнатурата на метода. `render()` или `action()`. Така че, ако сме на `Product:show` и `id:123`, връзката към `this` също ще предаде този параметър. +В същото време всички параметри, посочени в сигнатурата на `action()` или `render()` ако методът `action()` не са дефинирани, се прехвърлят. Така че, ако се намираме на страниците `Product:show` и `id:123`, връзката към `this` също ще предаде този параметър. Разбира се, можете да зададете параметрите и директно: @@ -213,7 +213,7 @@ $url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); Ако искаме да направим препратка към презентаторите в шаблона на компонента, използваме тага `{plink}`: ```latte -главная страница +главная страница ``` или в кода @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Псевдоними .[#toc-aliases]{data-version:v3.2.2} +=============================================== + +Понякога е полезно да зададете лесно запомнящ се псевдоним на двойка презентатор:действие. Например, можете да наречете началната страница `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 +``` + +Във връзките те се записват с помощта на символа at, например: + +```latte +administration +``` + +Те се поддържат във всички методи, които работят с връзки, като например `redirect()` и други подобни. + + Невалидни връзки .[#toc-invalid-links] ====================================== @@ -257,6 +281,6 @@ LinkGenerator .[#toc-linkgenerator] LinkGenerator е услуга, която може да се подаде през конструктора и след това да се създадат връзки чрез метода 'link()'. -Има разлика в сравнение с водещите. LinkGenerator създава всички връзки като абсолютни URL адреси. Освен това няма "текущ презентатор", така че не можете да зададете само името на действието "link('default')" или относителни пътища към модули. +В сравнение с водещите има разлика. LinkGenerator създава всички връзки директно като абсолютни URL адреси. Също така няма "действителен презентатор", така че не можете просто да изброите името на действието `link('default')` като цел или да изброите относителни пътища към модули. Невалидните връзки винаги хвърлят изключение `Nette\Application\UI\InvalidLinkException`. diff --git a/application/bg/directory-structure.texy b/application/bg/directory-structure.texy new file mode 100644 index 0000000000..4d1b6066a4 --- /dev/null +++ b/application/bg/directory-structure.texy @@ -0,0 +1,526 @@ +Структура на директорията на приложението +***************************************** + +
    + +Как да проектирате ясна и мащабируема структура на директориите за проекти в Nette Framework? Ще ви покажем доказани практики, които ще ви помогнат да организирате кода си. Ще научите: + +- как **логически да структурирате** приложението в директории +- как да проектирате структурата така, че да **се мащабира добре** с разрастването на проекта +- кои са **възможните алтернативи** и техните предимства или недостатъци + +
    + + +Важно е да се спомене, че самата Nette Framework не настоява за някаква специфична структура. Тя е проектирана така, че да може лесно да се адаптира към всякакви нужди и предпочитания. + + +Основна структура на проекта .[#toc-basic-project-structure] +============================================================ + +Въпреки че 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 база. + + +Принципи на организация на кода .[#toc-code-organization-principles] +==================================================================== + +Когато за първи път се запознаете с нов проект, трябва да можете бързо да се ориентирате. Представете си, че щракнете върху директорията `app/Model/` и видите тази структура: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +От нея ще научите само, че проектът използва някои услуги, хранилища и същности. Няма да научите нищо за действителното предназначение на приложението. + +Нека разгледаме друг подход - **организация по домейни**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Това е различно - от пръв поглед е ясно, че това е сайт за електронна търговия. Самите имена на директориите разкриват какво може да прави приложението - то работи с плащания, поръчки и продукти. + +Първият подход (организация по тип клас) носи няколко проблема на практика: кодът, който е логически свързан, е разпръснат в различни папки и трябва да се прескача между тях. Затова ще организираме по области. + + +Пространства от имена .[#toc-namespaces] +---------------------------------------- + +Прието е структурата на директориите да съответства на пространствата от имена в приложението. Това означава, че физическото местоположение на файловете съответства на тяхното пространство от имена. Например, клас, разположен в `app/Model/Product/ProductRepository.php`, трябва да има пространство от имена `App\Model\Product`. Този принцип помага за ориентацията на кода и опростява автоматичното зареждане. + + +Единствено и множествено число в имената .[#toc-singular-vs-plural-in-names] +---------------------------------------------------------------------------- + +Забележете, че използваме единствено число за главните директории на приложенията: `app`, `config`, `log`, `temp`, `www`. Същото се отнася и за вътрешността на приложението: `Model`, `Core`, `Presentation`. Това е така, защото всяка от тях представлява едно обединено понятие. + +По подобен начин `app/Model/Product` представя всичко за продуктите. Не го наричаме `Products`, защото това не е папка, пълна с продукти (която би съдържала файлове като `iphone.php`, `samsung.php`). Това е пространство от имена, което съдържа класове за работа с продукти - `ProductRepository.php`, `ProductService.php`. + +Папката `app/Tasks` е в множествено число, защото съдържа набор от самостоятелно изпълними скриптове - `CleanupTask.php`, `ImportTask.php`. Всеки от тях е самостоятелна единица. + +За по-голяма последователност препоръчваме да се използва: +- единствено число за пространства от имена, представляващи функционална единица (дори ако работите с множество единици) +- Множествено число за колекции от независими единици +- В случай на несигурност или ако не искате да мислите за това, изберете единствено число + + +Публичен указател `www/` .[#toc-public-directory-www] +===================================================== + +Тази директория е единствената, достъпна от уеб (т.нар. document-root). Често може да срещнете името `public/` вместо `www/` - това е просто въпрос на конвенция и не влияе на функционалността. Директорията съдържа: +- [Входна точка на |bootstrap#index.php] приложението `index.php` +- Файл `.htaccess` с правила за mod_rewrite (за Apache) +- Статични файлове (CSS, JavaScript, изображения) +- Качвани файлове + +За правилната сигурност на приложението е изключително важно да имате правилно [конфигуриран document-root |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url]. + +.[note] +Никога не поставяйте папката `node_modules/` в тази директория - тя съдържа хиляди файлове, които може да са изпълними и не трябва да са публично достъпни. + + +Директория за приложения `app/` .[#toc-application-directory-app] +================================================================= + +Това е основната директория с кода на приложението. Основна структура: + +/--pre +app/ +├── Core/ ← инфраструктурни въпроси +├── Model/ ← бизнес логика +├── Presentation/ ← презентатори и шаблони +├── Tasks/ ← командни скриптове +└── Bootstrap.php ← клас за зареждане на приложения +\-- + +`Bootstrap.php` е [класът за стартиране на приложението, |bootstrap] който инициализира средата, зарежда конфигурацията и създава DI контейнера. + +Нека сега да разгледаме подробно отделните поддиректории. + + +Презентатори и шаблони .[#toc-presenters-and-templates] +======================================================= + +В директорията `app/Presentation` се намира презентационната част на приложението. Алтернатива е краткият адрес `app/UI`. Това е мястото за всички презентатори, техните шаблони и всички помощни класове. + +Организираме този слой по домейни. В един сложен проект, който съчетава електронна търговия, блог и API, структурата би изглеждала по следния начин: + +/--pre +app/Presentation/ +├── Shop/ ← фронт за електронна търговия +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← блог +│ ├── Home/ +│ └── Post/ +├── Admin/ ← администрация +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← крайни точки на API + └── V1/ +\-- + +Обратно, за един прост блог бихме използвали тази структура: + +/--pre +app/Presentation/ +├── Front/ ← уебсайт frontend +│ ├── Home/ +│ └── Post/ +├── Admin/ ← администрация +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, карти на сайта и др. +\-- + +Папки като `Home/` или `Dashboard/` съдържат презентатори и шаблони. Папки като `Front/`, `Admin/` или `Api/` се наричат **модули**. Технически погледнато, това са обикновени директории, които служат за логическа организация на приложението. + +Всяка папка с презентатор съдържа презентатор с подобно име и неговите шаблони. Например папката `Dashboard/` съдържа: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← водещ +└── default.latte ← шаблон +\-- + +Тази структура на директориите е отразена в пространствата от имена на класовете. Например `DashboardPresenter` се намира в пространството от имена `App\Presentation\Admin\Dashboard` (вижте [съпоставянето на водещите |#presenter mapping]): + +```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`. + + +Разработване на гъвкава структура .[#toc-flexible-structure-development] +------------------------------------------------------------------------ + +Едно от големите предимства на тази структура е колко елегантно се адаптира към нарастващите нужди на проекта. Като пример нека вземем частта, генерираща XML емисии. Първоначално разполагаме с една проста форма: + +/--pre +Export/ +├── ExportPresenter.php ← един водещ за целия износ +├── sitemap.latte ← шаблон за карта на сайта +└── feed.latte ← шаблон за RSS канал +\-- + +С течение на времето се добавят повече типове емисии и ни е необходима повече логика за тях... Няма проблем! Папката `Export/` просто се превръща в модул: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← захранване за Amazon + └── ebay.latte ← емисия за eBay +\-- + +Тази трансформация е напълно безпроблемна - просто създайте нови подпапки, разделете кода в тях и актуализирайте връзките (напр. от `Export:feed` в `Export:Feed:amazon`). Благодарение на това можем постепенно да разширяваме структурата според нуждите, нивото на влагане не е ограничено по никакъв начин. + +Например, ако в администрацията имате много презентатори, свързани с управлението на поръчките, като `OrderDetail`, `OrderEdit`, `OrderDispatch` и т.н., можете да създадете модул (папка) `Order` за по-добра организация, който ще съдържа (папки за) презентатори `Detail`, `Edit`, `Dispatch` и други. + + +Местоположение на шаблона .[#toc-template-location] +--------------------------------------------------- + +В предишните примери видяхме, че шаблоните се намират директно в папката с презентатора: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← водещ +├── DashboardTemplate.php ← незадължителен клас на шаблона +└── default.latte ← шаблон +\-- + +Това местоположение се оказва най-удобно на практика - всички свързани с него файлове са ви под ръка. + +Като алтернатива можете да поставите шаблоните в подпапка на `templates/`. Nette поддържа и двата варианта. Можете дори да поставите шаблоните изцяло извън папката `Presentation/`. Всичко за опциите за местоположение на шаблоните можете да намерите в главата " [Търсене на шаблони" |templates#Template Lookup]. + + +Помощни класове и компоненти .[#toc-helper-classes-and-components] +------------------------------------------------------------------ + +Презентаторите и шаблоните често се предлагат с други помощни файлове. Разполагаме ги логично според техния обхват: + +1. **Напряко с презентатора** в случай на специфични компоненти за дадения презентатор: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← компонент за изписване на продукти +└── FilterForm.php ← форма за филтриране +\-- + +2. **За модул** - препоръчваме да използвате папката `Accessory`, която е поставена грижливо в началото на азбуката: + +/--pre +Front/ +├── Accessory/ +│ ├── NavbarControl.php ← компоненти за 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/`. А компонентите - в `app/Components`. Изборът зависи от конвенциите на екипа. + + +Модел - сърцето на приложението .[#toc-model-heart-of-the-application] +====================================================================== + +Моделът съдържа цялата бизнес логика на приложението. За неговата организация важи същото правило - структурираме по домейни: + +/--pre +app/Model/ +├── Payment/ ← всичко за плащанията +│ ├── PaymentFacade.php ← основна входна точка +│ ├── PaymentRepository.php +│ ├── Payment.php ← субект +├── Order/ ← всичко за поръчките +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← всичко за доставката +\-- + +В модела обикновено се срещат тези типове класове: + +**Фасади**: представляват основната входна точка към определена област в приложението. Те действат като оркестратор, който координира сътрудничеството между различни услуги за изпълнение на цялостни случаи на употреба (като "създаване на поръчка" или "обработка на плащане"). Под техния оркестрационен слой фасадата скрива подробностите за изпълнение от останалата част на приложението, като по този начин осигурява чист интерфейс за работа с дадения домейн. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // валидиране + // създаване на поръчки + // изпращане на имейл + // записване в статистиката + } +} +``` + +**Услуги**: фокусират се върху специфични бизнес операции в рамките на даден домейн. За разлика от фасадите, които оркестрират цели случаи на използване, услугата имплементира специфична бизнес логика (като изчисляване на цени или обработка на плащания). Услугите обикновено нямат състояние и могат да се използват или от фасади като градивни елементи за по-сложни операции, или директно от други части на приложението за по-прости задачи. + +```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 +// Субект, съпоставен с таблица от базата данни за поръчки +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, + ]); + } +} +``` + +**Стойностни обекти**: неизменни обекти, представляващи стойности без собствена идентичност - например парична сума или имейл адрес. Два екземпляра на стойностен обект с еднакви стойности се считат за идентични. + + +Код на инфраструктурата .[#toc-infrastructure-code] +=================================================== + +В папката `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` за изпращане на имейли за поръчки, знае техните шаблони и кога трябва да бъдат изпратени + + +Командни скриптове .[#toc-command-scripts] +========================================== + +Приложенията често трябва да изпълняват задачи извън обикновените 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 заявка, но трябва да се вземе предвид сигурността. Презентаторът, който изпълнява задачата, трябва да бъде защитен, например само за влезли в системата потребители или със силен токен и достъп от разрешени IP адреси. За дълги задачи е необходимо да се увеличи ограничението за време на скрипта и да се използва `session_write_close()`, за да се избегне заключване на сесията. + + +Други възможни директории .[#toc-other-possible-directories] +============================================================ + +В допълнение към споменатите основни директории можете да добавите и други специализирани папки в зависимост от нуждите на проекта. Нека разгледаме най-често срещаните от тях и тяхното използване: + +/--pre +app/ +├── Api/ ← Логика на API, независима от слоя за представяне +├── Database/ ← миграционни скриптове и посявки за тестови данни +├── Components/ ← споделени визуални компоненти за цялото приложение +├── Event/ ← полезно, ако се използва архитектура, управлявана от събития +├── Mail/ ← шаблони за имейли и свързана логика +└── Utils/ ← помощни класове +\-- + +За споделени визуални компоненти, използвани в презентаторите в цялото приложение, можете да използвате папката `app/Components` или `app/Controls`: + +/--pre +app/Components/ +├── Form/ ← споделени компоненти на формуляра +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← компоненти за списъци с данни +│ └── DataGrid.php +└── Navigation/ ← навигационни елементи + ├── Breadcrumbs.php + └── Menu.php +\-- + +В тази папка се намират компонентите с по-сложна логика. Ако искате да споделяте компоненти между няколко проекта, добре е да ги отделите в самостоятелен пакет на композитора. + +В директорията `app/Mail` можете да поставите управлението на имейл комуникацията: + +/--pre +app/Mail/ +├── templates/ ← шаблони за имейли +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Картографиране на водещия .[#toc-presenter-mapping] +=================================================== + +Съпоставянето дефинира правила за извеждане на имена на класове от имена на водещи. Посочваме ги в [конфигурацията |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 +``` + +Картографирането става чрез заместване на звездичката в маската `App\Presentation\*Presenter` с името на презентатора `Home`, в резултат на което се получава крайното име на класа `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 index 4325c76dd5..106277063b 100644 --- a/application/bg/how-it-works.texy +++ b/application/bg/how-it-works.texy @@ -22,18 +22,18 @@ /--pre web-project/ ├── app/ ← каталог с приложением -│ ├── Presenters/ ← классы презентеров -│ │ ├── HomePresenter.php ← Класс презентера главной страницы -│ │ └── templates/ ← директория шаблонов -│ │ ├── @layout.latte ← шаблон общего макета -│ │ └── Home/ ← шаблоны презентера главной страницы -│ │ └── default.latte ← шаблон действия `default` -│ ├── Router/ ← конфигурация URL-адресов +│ ├── Core/ ← основни необходими класове +│ │ └── RouterFactory.php ← конфигуриране на URL адреси +│ ├── Presentation/ ← презентатори, шаблони и др. +│ │ ├── @layout.latte ← шаблон на споделено оформление +│ │ └── Home/ ← Директория за водещи +│ │ ├── HomePresenter.php ← Клас на Home Presenter +│ │ └── default.latte ← шаблон за действие default │ └── Bootstrap.php ← загрузочный класс Bootstrap ├── bin/ ← скрипты командной строки ├── config/ ← файлы конфигурации │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← журналы ошибок ├── temp/ ← временные файлы, кэш, … ├── vendor/ ← библиотеки, установленные через Composer @@ -45,9 +45,9 @@ └── .htaccess ← запрещает доступ ко всем каталогам, кроме www \-- -Можете да променяте структурата на директориите по всякакъв начин, да преименувате или премествате папки и след това просто да редактирате пътищата до `log/` и `temp/` във файла `Bootstrap.php` и пътя до този файл в `composer.json` в раздела `autoload`. Нищо друго, никакво сложно преконфигуриране, никакви постоянни промени. Nette има [интелигентно автоматично откриване |bootstrap#development-vs-production-mode]. +Можете да променяте структурата на директориите по свой вкус, да преименувате или премествате папки - всичко е напълно гъвкаво. Nette разполага и с интелигентно автоматично откриване и автоматично разпознава местоположението на приложението, включително неговата URL база. -За малко по-големи приложения можем да разделим главната папка и папките с шаблони на подпапки (на диска) и пространства от имена (в кода), които наричаме [модули |modules]. +За малко по-големи приложения можем да организираме папките с презентатори и шаблони в [поддиректории |directory-structure#Presenters and templates] и да групираме класовете в пространства от имена, които наричаме модули. Публичната директория `www/` може да бъде променена, без да се налага да инсталирате нещо друго. Всъщност често се случва, че поради спецификата на вашия хостинг ще трябва да я преименувате или да инсталирате т.нар. document-root към тази директория в конфигурацията на хостинга. Ако хостингът ви не позволява да създавате папки на едно ниво над публичната директория, предлагаме ви да потърсите друга хостинг услуга. В противен случай ще се изложите на значителен риск за сигурността. @@ -75,7 +75,7 @@ HTTP заявка .[#toc-http-request] Какъв вид фабрика? Ние не произвеждаме трактори, а уебсайтове! Изчакайте, след малко ще бъде обяснено. -Под "инициализиране на средата" разбираме например активирането на услугата [Tracy |tracy:], която е невероятен инструмент за регистриране или визуализиране на грешки. Той регистрира грешките на производствения сървър и ги показва директно на сървъра за разработка. Затова по време на инициализацията трябва да решите дали сайтът ще работи в производствен режим или в режим за разработчици. Nette използва автоматично откриване за това: ако стартирате сайта на localhost, той се стартира в режим за разработчици. Не е необходимо да конфигурирате каквото и да било и приложението е готово както за разработка, така и за внедряване в производството. Тези стъпки се следват и са описани подробно в главата [Bootstrap |bootstrap]. +Под "инициализация на средата" разбираме например активирането на [Tracy |tracy:], който е фантастичен инструмент за регистриране и визуализиране на грешки. На производствени сървъри той регистрира грешките, а на сървърите за разработка ги показва директно. Следователно инициализацията включва определяне дали уебсайтът работи в производствен режим или в режим на разработка. За тази цел Nette използва [интелигентно автоматично разпознаване |bootstrap#development-vs-production-mode]: ако стартирате сайта на localhost, той работи в режим на разработка. Не е необходима конфигурация и приложението е готово както за разработка, така и за производствено внедряване. Тези стъпки се изпълняват и са описани подробно в главата за [класа Bootstrap |bootstrap]. Третата точка (да, пропуснахме втората, но ще се върнем към нея) е да стартирате приложението. Класът `Nette\Application\Application` (наричан по-нататък `Application`) обработва HTTP заявките в Nette, така че когато казваме "стартиране на приложение", имаме предвид извикване на метод с име `run()` върху обект от този клас. @@ -91,7 +91,7 @@ Nette е наставник, който ви напътства да пишет Приложението започва с искане към т.нар. маршрутизатор да реши на кой от презентаторите да изпрати текущата заявка за обработка. Маршрутизаторът решава чия е отговорността. Той разглежда входния URL адрес `https://example.com/product/123`, който иска продукт `показать` с `id: 123` като действие. Добър навик е да записвате двойките водещ + действие, разделени с двоеточие: `Продукт:показать`. -Следователно маршрутизаторът е преобразувал URL адреса в двойка `Presenter:action` + параметри, в нашия случай `Product:show` + `id`: 123`. Вы можете увидеть, как выглядит маршрутизатор в файле `app/Router/RouterFactory.php`, и ще го опишем подробно в главата [Маршрутизация |routing]. +Следователно маршрутизаторът е преобразувал URL адреса в двойка `Presenter:action` + параметри, в нашия случай `Product:show` + `id`: 123`. Вы можете увидеть, как выглядит маршрутизатор в файле `app/Core/RouterFactory.php`, и ще го опишем подробно в главата [Маршрутизация |routing]. Да продължим. Приложението вече знае името на водещия и може да продължи. Чрез създаване на обект `ProductPresenter`, който е кодът на предентера `Product`. По-точно, той иска от контейнера DI да създаде презентатора, тъй като създаването на обекти е негова работа. @@ -121,12 +121,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter След това водещият връща отговор. Това може да бъде HTML страница, изображение, XML документ, файл, изпратен от диска, JSON или пренасочване към друга страница. Важно е да се отбележи, че ако не посочим изрично как да се отговори (какъвто е случаят с `ProductPresenter`), отговорът ще бъде шаблон, показващ HTML страница. Защо? Ами защото в 99% от случаите искаме да покажем шаблон, водещият приема това поведение по подразбиране и иска да улесни работата ни. Това е гледната точка на Нете. -Дори не е необходимо да указваме кой шаблон да се покаже, той сам извежда пътя до него според проста логика. В случая с водещия `Product` и действието `show`, той се опитва да провери дали някой от тези файлове с шаблони съществува спрямо директорията, в която се намира класът `ProductPresenter`: +Дори не е необходимо да посочваме кой шаблон да се визуализира; рамката сама ще определи пътя. В случая с действието `show` тя просто се опитва да зареди шаблона `show.latte` в директорията с класа `ProductPresenter`. Тя също така се опитва да намери оформлението във файла `@layout.latte` (повече за [търсенето на шаблони |templates#Template Lookup]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -След това се показва шаблонът. Задачата на водещия и на цялото приложение вече е изпълнена. Ако шаблонът не съществува, ще бъде върната страница за грешка 404. Можете да прочетете повече за водещите на страницата [Водещи |presenters]. +Впоследствие шаблоните се визуализират. С това задачата на презентатора и на цялото приложение е изпълнена и работата е приключила. Ако шаблонът не съществува, ще бъде върната страница с грешка 404. Можете да прочетете повече за презентаторите на страницата [Презентатори |presenters]. [* request-flow.svg *] @@ -137,7 +134,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter 3) маршрутизаторът декодира URL адреса като двойка `Home:default` 4) обектът е създаден `HomePresenter` 5) извиква се методът `renderDefault()` (ако съществува) -6) шаблонът `templates/Home/default.latte` с оформлението `templates/@layout.latte` се визуализира +6) шаблонът `default.latte` с оформлението `@layout.latte` се визуализира Може би сега ще се сблъскате с много нови концепции, но ние смятаме, че те имат смисъл. Създаването на приложения в Nette е лесно. diff --git a/application/bg/modules.texy b/application/bg/modules.texy deleted file mode 100644 index 622fd48b2e..0000000000 --- a/application/bg/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Модули -****** - -.[perex] -В Nette модулите са логическите единици, от които се състои едно приложение. Те включват главни модули, шаблони, евентуално компоненти и класове модели. - -Един компонент за презентатори и един за шаблони няма да са достатъчни за реални проекти. Натрупването на десетки файлове в една папка е меко казано неорганизирано. Как да излезем от тази ситуация? Просто ги разделяме на поддиректории на диска и на пространства от имена в кода. Точно това правят модулите Nette. - -Така че нека забравим за една папка за презентатори и шаблони и вместо това да създадем модули като `Admin` и `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← директория с модулями -│ ├── Admin/ ← модуль Admin -│ │ ├── Presenters/ ← его презентеры -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← модуль Front -│ └── Presenters/ ← его презентеры -│ └── ... -\-- - -Тази структура на директориите ще бъде отразена в пространствата за имена на класовете, така че например `DashboardPresenter` ще бъде в пространството `App\Modules\Admin\Presenters`: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Главното устройство `Dashboard` в модула `Admin` се обозначава в приложението с помощта на запис с двойна точка като `Admin:Dashboard`, а неговото действие `default` се обозначава като `Admin:Dashboard:default`. -И откъде Nette знае, че `Admin:Dashboard` представлява класа `App\Modules\Admin\Presenters\DashboardPresenter`? Говорим за това, като използваме [картографирането |#Mapping] в конфигурацията. -Така че дадената структура не е фиксирана и можете да я променяте по свое усмотрение. - -Модулите, разбира се, могат да съдържат всички други части, освен презентатори и шаблони, като компоненти, класове модели и др. - - -Вложени модули .[#toc-nested-modules] -------------------------------------- - -Модулите не трябва да образуват само плоска структура, можете да създавате и подмодули, например: - -/--pre -app/ -├── Modules/ ← директория с модулями -│ ├── Blog/ ← модуль Blog -│ │ ├── Admin/ ← подмодуль Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← подмодуль Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← модуль Forum -│ │ └── ... -\-- - -Така модулът `Blog` се разделя на подмодули `Admin` и `Front`. Това отново ще бъде отразено в пространствата от имена, които ще бъдат `App\Modules\Blog\Admin\Presenters` и т.н. Главният модул `Dashboard` в рамките на подмодула се нарича `Blog:Admin:Dashboard`. - -Разклоненията могат да бъдат толкова дълбоки, колкото искате, така че можете да създавате подмодули. - - -Създаване на връзки .[#toc-creating-links] ------------------------------------------- - -Връзките в главните шаблони са относителни към текущия модул. По този начин връзка `Foo:default` води до главния `Foo` в същия модул като текущия главен. Например, ако текущият модул е `Front`, връзката изглежда по следния начин - -```latte -odkaz na Front:Product:show -``` - -Връзката е относителна, дори ако името на модула е част от нея, тогава той се счита за подмодул: - -```latte -odkaz na Front:Shop:Product:show -``` - -Абсолютните връзки се записват подобно на абсолютните пътища на диска, но с двоеточие вместо с наклонена черта. Така абсолютната връзка започва с двоеточие: - -```latte -odkaz na Admin:Product:show -``` - -За да разберем дали се намираме в определен модул или подмодул, използваме функцията `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Маршрутизиране .[#toc-routing] ------------------------------- - -Вижте [главата за маршрутизиране |routing#modules]. - - -Картографиране .[#toc-mapping] ------------------------------- - -Определя правилата, по които името на класа се извежда от главното име. Записваме ги в [конфигурацията |configuration] под ключа `application › mapping`. - -Нека започнем с пример, при който не се използват модули. Искаме само главните класове да имат пространството от имена `App\Presenters`. Това означава, че искаме главното име, например `Home`, да се съпостави с класа `App\Presenters\HomePresenter`. Това може да се постигне със следната конфигурация: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -Името на водещия се заменя със звездичка и резултатът е името на класа. Лесно! - -Ако разделим презентаторите на модули, можем да използваме различни карти за всеки модул: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Сега водещият `Front:Home` е определен от класа `App\Modules\Front\HomePresenter`, а презентер `Admin:Dashboard` - `App\AdminModule\DashboardPresenter`. - -Би било по-удобно да се създаде общо правило (звездичка), което да замени първите две правила, и да се добави допълнителна звездичка само за модула: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Но какво става, ако използваме няколко вложени модула и имаме например главен модул `Admin:User:Edit`? В този случай сегментът със звездичка, представляващ модула за всяко ниво, просто ще се повтори и резултатът ще бъде класът `App\Modules\Admin\User\Presenters\EditPresenter`. - -Алтернативен начин за записване е използването на масив от три сегмента вместо низ. Този запис е еквивалентен на предишния: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -Стойността по подразбиране е `*: *Module\*Presenter`. diff --git a/application/bg/presenters.texy b/application/bg/presenters.texy index 53606e891f..bf2aeb04fc 100644 --- a/application/bg/presenters.texy +++ b/application/bg/presenters.texy @@ -60,7 +60,7 @@ class ArticlePresenter extends Nette\Application\UI\Presenter Важното е, че `action()` се извиква преди `render()`, така че в него можем евентуално да променим следващия жизнен цикъл, т.е. да променим шаблона за визуализиране и метода `render()`която ще бъде извикана с помощта на `setView('otherView')`. -Параметрите от заявката се предават на метода. Възможно и препоръчително е да се посочат типове за параметрите, например `actionShow(int $id, string $slug = null)` - ако параметърът `id` липсва или ако не е цяло число, презентаторът ще върне [грешка 404 |#Error-404-etc] и ще прекрати операцията. +Параметрите от заявката се предават на метода. Възможно и препоръчително е да се посочат типове за параметрите, например `actionShow(int $id, ?string $slug = null)` - ако параметърът `id` липсва или ако не е цяло число, презентаторът ще върне [грешка 404 |#Error-404-etc] и ще прекрати операцията. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ $this->redirect(/* ... */); Грешка 404 и т.н. .[#toc-error-404-etc] ======================================= -Когато не можем да изпълним дадена заявка, защото например статията, която искаме да покажем, не съществува в базата данни, ще хвърлим грешка 404, като използваме метода `error(string $message = null, int $httpCode = 404)`, който представлява HTTP грешка 404: +Когато не можем да изпълним дадена заявка, защото например статията, която искаме да покажем, не съществува в базата данни, ще хвърлим грешка 404, като използваме метода `error(?string $message = null, int $httpCode = 404)`, който представлява HTTP грешка 404: ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Параметри на заявката .[#toc-request-parameters] +================================================ + +Презентаторът, както и всеки компонент, получава своите параметри от HTTP заявката. Стойностите им могат да бъдат извлечени с помощта на метода `getParameter($name)` или `getParameters()`. Стойностите са низове или масиви от низове, по същество сурови данни, получени директно от URL адреса. + +За по-голямо удобство препоръчваме да направите параметрите достъпни чрез свойства. Просто ги анотирайте с `#[Parameter]` атрибут: + +```php +use Nette\Application\Attributes\Parameter; // този ред е важен + +class HomePresenter extends Nette\Application\UI\Presenter +{ + #[Parameter] + public string $theme; // трябва да е публичен +} +``` + +За свойствата предлагаме да посочите типа данни (например `string`). След това Nette автоматично ще определи стойността въз основа на него. Стойностите на параметрите също могат да бъдат [валидирани |#Validation of Parameters]. + +Когато създавате връзка, можете директно да зададете стойност за параметрите: + +```latte +click +``` + + Постоянни параметри .[#toc-persistent-parameters] ================================================= @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Ако `$this->lang` има стойност като `'en'`, то връзките, създадени с помощта на `link()` или `n:href`, ще съдържат и параметъра `lang=en`. И когато върху връзката се щракне, тя отново ще бъде `$this->lang = 'en'`. -За свойствата препоръчваме да включите типа данни (например `string`), а също така можете да включите стойност по подразбиране. Стойностите на параметрите могат да бъдат [валидирани |#Validation of Persistent Parameters]. +За свойствата препоръчваме да включите типа на данните (например `string`), като можете да включите и стойност по подразбиране. Стойностите на параметрите могат да бъдат [валидирани |#Validation of Parameters]. Постоянните параметри се предават между всички действия на даден презентатор по подразбиране. За да ги предадете между няколко водещи, трябва да ги дефинирате или: @@ -307,18 +333,12 @@ class ProductPresenter extends Nette\Application\UI\Presenter Това, което показахме досега в тази глава, вероятно ще бъде достатъчно. Следващите редове са за тези, които се интересуват от презентаторите обстойно и искат да знаят всичко. -Изисквания и параметри .[#toc-requirement-and-parameters] ---------------------------------------------------------- +Утвърждаване на параметрите .[#toc-validation-of-parameters] +------------------------------------------------------------ -Заявката, която се обработва от водещия, е обектът [api:Nette\Application\Request] и се връща от метода на водещия `getRequest()`. Тя включва масив от параметри и всеки от тях принадлежи или на някой от компонентите, или директно на водещия (който всъщност също е компонент, макар и специален). Затова Nette преразпределя параметрите и преминава между отделните компоненти (и водещия), като извиква метода `loadState(array $params)`. Параметрите могат да бъдат получени чрез метода `getParameters(): array`, а поотделно чрез `getParameter($name)`. Стойностите на параметрите са низове или масиви от низове, те по същество са необработени данни, получени директно от URL адреса. +Стойностите на [параметрите на заявката |#request parameters] и [постоянните параметри, |#persistent parameters] получени от URL адреси, се записват в свойствата чрез метода `loadState()`. Той също така проверява дали типът данни, посочен в свойството, съвпада, в противен случай ще отговори с грешка 404 и страницата няма да бъде показана. - -Утвърждаване на постоянни параметри .[#toc-validation-of-persistent-parameters] -------------------------------------------------------------------------------- - -Стойностите на [постоянните параметри, |#persistent parameters] получени от URL адреси, се записват в свойствата чрез метода `loadState()`. Той също така проверява дали типът данни, посочен в свойството, съвпада, в противен случай ще отговори с грешка 404 и страницата няма да бъде показана. - -Никога не се доверявайте сляпо на постоянните параметри, тъй като те лесно могат да бъдат пренаписани от потребителя в URL адреса. Например, така проверяваме дали `$this->lang` е сред поддържаните езици. Добър начин да направите това е да пренастроите метода `loadState()`, споменат по-горе: +Никога не се доверявайте сляпо на параметрите, тъй като те лесно могат да бъдат пренаписани от потребителя в URL адреса. Например, така проверяваме дали `$this->lang` е сред поддържаните езици. Добър начин да направите това е да презапишете метода `loadState()`, споменат по-горе: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,7 +361,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Запазване и възстановяване на заявка .[#toc-save-and-restore-the-request] ------------------------------------------------------------------------- -Можете да запазите текущата заявка в сесия или да я възстановите от сесия и да позволите на водещия да я изпълни отново. Това е полезно, например когато потребителят попълни формуляр и срокът му на влизане изтече. За да избегнем загубата на данни, преди да пренасочим към страницата за регистрация, запазваме текущата заявка в сесията с функцията `$reqId = $this->storeRequest()`, която връща идентификатора като кратък низ и го предава като параметър на водещия за регистрация. +Заявката, която се обработва от водещия, е обект [api:Nette\Application\Request] и се връща от метода на водещия `getRequest()`. + +Можете да запазите текущата заявка в сесия или да я възстановите от сесия и да позволите на водещия да я изпълни отново. Това е полезно, например, когато потребител попълни формуляр и срокът на неговото влизане в системата изтече. За да не загубим данни, преди да пренасочим към страницата за вписване, запазваме текущата заявка в сесия с помощта на `$reqId = $this->storeRequest()`, която връща идентификатор под формата на кратък низ и го предава като параметър на презентатора за вписване. След като влезем в системата, извикваме метода `$this->restoreRequest($reqId)`, който извлича заявката от сесията и я препраща към нея. Методът проверява дали заявката е създадена от същия потребител, който в момента е влязъл в системата. Ако е влязъл друг потребител или ключът е невалиден, не се прави нищо и програмата продължава. @@ -362,7 +384,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Можете също така да извикате канонизацията ръчно с метода `canonicalize()`, който, както и методът `link()`, приема като аргументи водещия, действията и параметрите. Тя създава връзка и я сравнява с текущия URL адрес. Ако те са различни, се пренасочва към генерираната връзка. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // пренасочва, ако $slug е различен от $realSlug @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Ограничаване на достъпа с помощта на `#[Requires]` .[#toc-access-restriction-using-requires]{data-version:3.2.2} +---------------------------------------------------------------------------------------------------------------- + +В `#[Requires]` предоставя разширени опции за ограничаване на достъпа до презентаторите и техните методи. Той може да се използва за определяне на HTTP методи, изискване на AJAX заявки, ограничаване на достъпа до същия произход и ограничаване на достъпа само до препращане. Атрибутът може да се прилага към класове на презентатори, както и към отделни методи, като например `action()`, `render()`, `handle()`, и `createComponent()`. + +Можете да зададете тези ограничения: +- на HTTP методите: `#[Requires(methods: ['GET', 'POST'])]` +- изискващи AJAX заявка: `#[Requires(ajax: true)]` +- достъп само от един и същ произход: `#[Requires(sameOrigin: true)]` +- достъп само чрез препращане: `#[Requires(forward: true)]` +- ограничения за конкретни действия: `#[Requires(actions: 'default')]` + +За подробности вижте [Как да използвате Requires атрибут |best-practices:attribute-requires]. + + +Проверка на метода HTTP .[#toc-http-method-check] +------------------------------------------------- + +В 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 заявка, която браузърите автоматично изпращат преди действителната заявка, когато е необходимо да се определи дали заявката е разрешена от гледна точка на политиката CORS (Cross-Origin Resource Sharing). Ако разрешите този метод, но не приложите подходящ отговор, това може да доведе до несъответствия и потенциални проблеми със сигурността. + + Допълнително четене .[#toc-further-reading] =========================================== diff --git a/application/bg/routing.texy b/application/bg/routing.texy index 300a7755e2..e7be8b7197 100644 --- a/application/bg/routing.texy +++ b/application/bg/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Или можем да използваме тази форма, като отбележим пренаписването на израза за регулярна проверка: +За по-подробна спецификация може да се използва още по-разширена форма, в която освен стойностите по подразбиране могат да се задават и други свойства на параметъра, например регулярен израз за валидиране (вж. параметъра `id` ): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Тези по-подробни формати са полезни за добавяне на повече метаданни. +Важно е да се отбележи, че ако параметрите, дефинирани в масива, не са включени в маската на пътя, техните стойности не могат да бъдат променени, дори и чрез използване на параметри на заявката, зададени след въпросителен знак в URL адреса. Филтри и преводи .[#toc-filters-and-translations] @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Модули .[#toc-modules] ---------------------- -Ако имаме няколко маршрута, принадлежащи към един и същ модул, можем да използваме `withModule()`, за да ги групираме: +Ако имаме повече маршрути, които принадлежат на един [модул |directory-structure#Presenters and Templates], можем да използваме `withModule()`, за да ги групираме: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Интеграция .[#toc-integration] ============================== -За да свържем маршрутизатора си с приложението, трябва да информираме за това контейнера DI. Най-лесният начин е да се подготви фабрика, която ще създаде обект маршрутизатор, и да се каже на конфигурацията на контейнера да го използва. Да предположим, че напишем метод за това, `App\Router\RouterFactory::createRouter()`: +За да свържем маршрутизатора си с приложението, трябва да информираме за това контейнера DI. Най-лесният начин е да се подготви фабрика, която ще създаде обект маршрутизатор, и да се каже на конфигурацията на контейнера да го използва. Да предположим, че напишем метод за това, `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ class RouterFactory ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Всички зависимости, като например връзки към бази данни и т.н., се предават на метода на фабриката като параметри, като се използва [автоматично свързване |dependency-injection:autowiring]: @@ -663,7 +663,7 @@ $router->addRoute(/* ... */); Затова отново ще добавим метод, който ще създаде например маршрутизатор: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Или ще създадем обектите директно: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/bg/templates.texy b/application/bg/templates.texy index 850d3e2ecd..f0ddd26ce9 100644 --- a/application/bg/templates.texy +++ b/application/bg/templates.texy @@ -34,35 +34,81 @@ Nette използва системата за шаблони [Latte |latte:]. L Той дефинира блок `content`, който се вмъква вместо `{include content}` в оформлението, и замества блока `title`, който презаписва `{block title}` в оформлението. Опитайте се да си представите резултата. -Търсене на шаблони .[#toc-search-for-templates] ------------------------------------------------ +Търсене на шаблони .[#toc-template-lookup] +------------------------------------------ + +В презентаторите не е необходимо да посочвате кой шаблон трябва да бъде визуализиран; рамката автоматично ще определи пътя, което ще ви улесни при кодирането. + +Ако използвате структура от директории, в която всеки презентатор има своя собствена директория, просто поставете шаблона в тази директория под името на действието (т.е. изглед). Например, за действието `default` използвайте шаблона `default.latte`: -Пътят към шаблоните се определя от главния модул с помощта на проста логика. Той ще се опита да провери дали има някой от тези файлове, разположен спрямо главната директория на класа, където `` е името на текущия главен модул, а `` е името на текущото събитие: +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -- `templates//.latte` -- `templates/..latte` +Ако използвате структура, в която презентаторите са заедно в една директория, а шаблоните - в папка `templates`, запишете я или във файл `..latte` или `/.latte`: -Ако шаблонът не бъде намерен, ще се опита да търси в директорията `templates` едно ниво по-нагоре, т.е. на същото ниво като директорията с класа на водещия. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- -Ако шаблонът не бъде намерен и там, отговорът ще бъде [грешка 404 |presenters#Error 404 etc.]. +Директорията `templates` може да бъде поставена и едно ниво по-нагоре, на същото ниво като директорията с класовете на водещите. -Можете също така да промените изгледа с помощта на `$this->setView('jineView')`. Или вместо да търсите директно, посочете името на файла с шаблона, като използвате `$this->template->setFile('/path/to/template.latte')`. +Ако шаблонът не бъде намерен, презентаторът отговаря с [грешка 404 - страница не е намерена |presenters#Error 404 etc]. + +Можете да промените изгледа, като използвате `$this->setView('anotherView')`. Възможно е също така директно да посочите файла с шаблона с помощта на `$this->template->setFile('/path/to/template.latte')`. .[note] -Файловете, които се търсят за шаблони, могат да се променят чрез наслагване на метода [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], който връща масив от възможни имена на файлове. +Файловете, в които се търсят шаблони, могат да се променят чрез надграждане на метода [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], който връща масив от възможни имена на файлове. + + +Търсене на шаблони за оформление .[#toc-layout-template-lookup] +--------------------------------------------------------------- + +Nette също така автоматично търси файла с оформлението. + +Ако използвате структура на директориите, в която всеки водещ има своя собствена директория, поставете макета или в папката с водещия, ако е специфичен само за него, или на по-високо ниво, ако е общ за няколко водещи: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +Ако използвате структура, в която презентаторите са групирани в една директория, а шаблоните са в папка `templates`, макетът ще се очаква на следните места: -В тези файлове се очаква оформление: +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` Разположение, общо за няколко високоговорителя +Ако презентаторът се намира в модул, той ще търси и по-нагоре в дървото на директориите в съответствие с вложеността на модула. -Къде: `` е името на текущия водещ, а `` е името на оформлението, което по подразбиране е `'layout'`. Името може да бъде променено с помощта на `$this->setLayout('jinyLayout')`, така че ще бъдат изпробвани файлове `@jinyLayout.latte`. +Името на макета може да бъде променено с помощта на `$this->setLayout('layoutAdmin')` и тогава то ще бъде очаквано във файла `@layoutAdmin.latte`. Можете също така директно да посочите файла с шаблона на оформлението, като използвате `$this->setLayout('/path/to/template.latte')`. -Можете също така директно да посочите името на файла на шаблона за оформление, като използвате `$this->setLayout('/path/to/template.latte')`. Използването на `$this->setLayout(false)` деактивира проследяването на оформлението. +Използването на `$this->setLayout(false)` или на тага `{layout none}` вътре в шаблона деактивира търсенето на оформление. .[note] -Файловете, в които се търсят шаблоните за оформление, могат да се променят чрез наслагване на метода [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], който връща масив от възможни имена на файлове. +Файловете, в които се търсят шаблони за оформление, могат да бъдат променяни чрез надграждане на метода [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], който връща масив от възможни имена на файлове. Променливи в шаблона .[#toc-variables-in-the-template] @@ -104,7 +150,7 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template Можете също така да си позволите лукса да шепнете в шаблоните, просто инсталирайте плъгина Latte в PhpStorm и поставете името на класа в началото на шаблона, за повече информация вижте статията "Latte: как да въведем системата":https://blog.nette.org/bg/latte-kak-da-izpolzvame-sistemata-ot-tipove: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void Версия 3 на Latte предлага по-усъвършенстван начин за създаване на [разширение за |latte:creating-extension] всеки уеб проект. Ето кратък пример за такъв клас: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ final class LatteExtension extends Latte\Extension ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ protected function beforeRender(): void ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` След това транслаторът може да се използва например като филтър `|translate`, като на метода `translate()` се предават допълнителни параметри (вж. `foo, bar`): diff --git a/application/cs/@home.texy b/application/cs/@home.texy index 833406fcbd..9bbdba7d6b 100644 --- a/application/cs/@home.texy +++ b/application/cs/@home.texy @@ -2,19 +2,7 @@ Nette Application ***************** .[perex] -Balíček `nette/application` představuje základ pro tvorbu interaktivních webových aplikací. - -- [Jak fungují aplikace? |how-it-works] -- [Bootstrap] -- [Presentery |presenters] -- [Šablony |templates] -- [Moduly |modules] -- [Routování |routing] -- [Vytváření odkazů URL |creating-links] -- [Interaktivní komponenty |components] -- [AJAX & snippety |ajax] -- [Multiplier |multiplier] -- [Konfigurace |configuration] +Nette Application je jádrem frameworku Nette, které přináší výkonné nástroje pro vytváření moderních webových aplikací. Nabízí řadu výjimečných vlastností, které výrazně usnadňují vývoj a zlepšují bezpečnost i udržovatelnost kódu. Instalace @@ -26,10 +14,71 @@ Knihovnu stáhnete a nainstalujete pomocí nástroje [Composer|best-practices:co composer require nette/application ``` + +Proč zvolit Nette Application? +------------------------------ + +Nette bylo vždy průkopníkem v oblasti webových technologií. + +**Obousměrný router:** Nette disponuje pokročilým routovacím systémem, který je unikátní svou obousměrností - nejen že překládá URL na akce aplikace, ale také dokáže zpětně generovat URL adresy. To znamená, že: +- Můžete kdykoliv změnit strukturu URL celé aplikace bez nutnosti upravovat šablony +- URL jsou automaticky kanonizovány, což zlepšuje SEO +- Routování je definováno na jednom místě, nikoliv roztroušeně v anotacích + +**Komponenty a signály:** Vestavěný komponentový systém inspirovaný Delphi a React.js je mezi PHP frameworky zcela výjimečný: +- Umožňuje vytvářet znovupoužitelné UI prvky +- Podporuje hierarchické skládání komponent +- Nabízí elegantní zpracování AJAX požadavků pomocí signálů +- Bohatá knihovna hotových komponent na [Componette](https://componette.org) + +**AJAX a snippety:** Nette představilo revoluční způsob práce s AJAXem již v roce 2009, dlouho před podobnými řešeními jako Hotwire pro Ruby on Rails nebo Symfony UX Turbo: +- Snippety umožňují aktualizovat jen části stránky bez nutnosti psát JavaScript +- Automatická integrace s komponentovým systémem +- Chytrá invalidace částí stránek +- Minimální množství přenášených dat + +**Intuitivní šablony [Latte|latte:]:** Nejbezpečnější šablonovací systém pro PHP s pokročilými funkcemi: +- Automatická ochrana proti XSS s kontextově citlivým escapováním +- Rozšiřitelnost pomocí vlastních filtrů, funkcí a značek +- Dědičnost šablon a snippety pro AJAX +- Vynikající podpora PHP 8.x s typovým systémem + +**Dependency Injection:** Nette plně využívá Dependency Injection: +- Automatické předávání závislostí (autowiring) +- Konfigurace pomocí přehledného NEON formátu +- Podpora pro továrny na komponenty + + +Hlavní výhody +------------- + +- **Bezpečnost**: Automatická obrana proti [zranitelnostem|nette:vulnerability-protection] jako XSS, CSRF, atd. +- **Produktivita**: Méně psaní, více funkcí díky chytrému návrhu +- **Debugging**: [Tracy debugger|tracy:] s routovacím panelem +- **Výkon**: Chytrá cache, lazy loading komponent +- **Flexibilita**: Snadná úprava URL i po dokončení aplikace +- **Komponenty**: Unikátní systém znovupoužitelných UI prvků +- **Moderní**: Plná podpora PHP 8.4+ a typového systému + + +Začínáme +-------- + +1. [Jak fungují aplikace? |how-it-works] - Pochopení základní architektury +2. [Presentery |presenters] - Práce s presentery a akcemi +3. [Šablony |templates] - Tvorba šablon v Latte +4. [Routování |routing] - Konfigurace URL adres +5. [Interaktivní komponenty |components] - Využití komponentového systému + + +Kompatbility s PHP +------------------ + | verze | kompatibilní s PHP |-----------|------------------- -| Nette Application 4.0 | PHP 8.0 – 8.2 -| Nette Application 3.1 | PHP 7.2 – 8.2 +| Nette Application 4.0 | PHP 8.1 – 8.4 +| Nette Application 3.2 | PHP 8.1 – 8.4 +| Nette Application 3.1 | PHP 7.2 – 8.3 | Nette Application 3.0 | PHP 7.1 – 8.0 | Nette Application 2.4 | PHP 5.6 – 8.0 diff --git a/application/cs/@left-menu.texy b/application/cs/@left-menu.texy index f7378fff34..bd056f35be 100644 --- a/application/cs/@left-menu.texy +++ b/application/cs/@left-menu.texy @@ -4,7 +4,7 @@ Aplikace v Nette - [Bootstrap] - [Presentery |presenters] - [Šablony |templates] -- [Moduly |modules] +- [Adresářová struktura |directory-structure] - [Routování |routing] - [Vytváření odkazů URL |creating-links] - [Interaktivní komponenty |components] diff --git a/application/cs/ajax.texy b/application/cs/ajax.texy index e4d30f762a..327977ea19 100644 --- a/application/cs/ajax.texy +++ b/application/cs/ajax.texy @@ -3,10 +3,10 @@ AJAX & snippety
    -Moderní webové aplikace dnes běží napůl na serveru, napůl v prohlížeči. AJAX je tím klíčovým spojovacím prvkem. Jakou podporu nabízí Nette Framework? -- posílání výřezů šablony (tzv. snippety) +V éře moderních webových aplikací, kde se často rozkládá funkcionalita mezi serverem a prohlížečem, je AJAX nezbytným spojovacím prvkem. Jaké možnosti nám v této oblasti nabízí Nette Framework? +- odesílání částí šablony, tzv. snippetů - předávání proměnných mezi PHP a JavaScriptem -- debugování AJAXových aplikací +- nástroje pro debugování AJAXových požadavků
    @@ -14,29 +14,32 @@ Moderní webové aplikace dnes běží napůl na serveru, napůl v prohlížeči AJAXový požadavek ================= -AJAXový požadavek se nijak neliší od klasického požadavku - je zavolán presenter s určitým view a parametry. Je také věcí presenteru, jak bude na něj reagovat: může použít vlastní rutinu, která vrátí nějaký fragment HTML kódu (HTML snippet), XML dokument, JSON objekt nebo kód v JavaScriptu. +AJAXový požadavek se v zásadě neliší od klasického HTTP požadavku. Zavolá se presenter s určitými parametry. A je na presenteru, jakým způsobem bude na požadavek reagovat - může vrátit data ve formátu JSON, odeslat část HTML kódu, XML dokument, atd. -Na straně serveru lze AJAXový požadavek detekovat metodou služby [zapouzdřující HTTP požadavek |http:request] `$httpRequest->isAjax()` (detekuje podle HTTP hlavičky `X-Requested-With`). Uvnitř presenteru je k dispozici "zkratka" v podobě metody `$this->isAjax()`. +Na straně prohlížeče inicializujeme AJAXový požadavek pomocí funkce `fetch()`: -Pro odesílání dat prohlížeči ve formátu JSON lze využít předpřipravený objekt `payload`: - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // zpracování odpovědi +}); ``` -Pokud potřebujete plnou kontrolu nad odeslaným JSONem, použijte metodu `sendJson` v presenteru. Tím ihned ukončíte činnost presenteru a obejdete se i bez šablony: +Na straně serveru rozpoznáme AJAXový požadavek metodou `$httpRequest->isAjax()` služby [zapouzdřující HTTP požadavek |http:request]. K detekci používá HTTP hlavičku `X-Requested-With`, proto je důležité ji odesílat. V rámci presenteru lze použít metodu `$this->isAjax()`. + +Chcete-li odeslat data ve formátu JSON, použijte metodu [`sendJson()` |presenters#Odeslání odpovědi]. Metoda rovněž ukončí činnost presenteru. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Když chceme odeslat HTML, můžeme jednak zvolit speciální šablonu pro AJAX: +Máte-li v plánu odpovědět pomocí speciální šablony určené pro AJAX, můžete to udělat následovně: ```php public function handleClick($param): void @@ -49,10 +52,20 @@ public function handleClick($param): void ``` +Snippety +======== + +Nejsilnější prostředek, který nabízí Nette pro propojení serveru s klientem, představují snippety. Díky nim můžete z obyčejné aplikace udělat AJAXovou jen s minimálním úsilím a několika řádky kódu. Jak to celé funguje demonstruje příklad Fifteen, jehož kód najdete na [GitHubu |https://github.com/nette-examples/fifteen]. + +Snippety, nebo-li výstřižky, umožnují aktualizovat jen části stránky, místo toho, aby se celá stránka znovunačítala. Jednak je to rychlejší a efektivnější, ale poskytuje to také komfortnější uživatelský zážitek. Snippety vám mohou připomínat Hotwire pro Ruby on Rails nebo Symfony UX Turbo. Zajímavé je, že Nette představilo snippety již o 14 let dříve. + +Jak snippety fungují? Při prvním načtení stránky (ne-AJAXovém požadavku) se načte celá stránka včetně všech snippetů. Když uživatel interaguje se stránkou (např. klikne na tlačítko, odešle formulář, atd.), místo načtení celé stránky se vyvolá AJAXový požadavek. Kód v presenteru provede akci a rozhodne, které snippety je třeba aktualizovat. Nette tyto snippety vykreslí a odešle ve formě pole ve formátu JSON. Obslužný kód v prohlížeči získané snippety vloží zpět do stránky. Přenáší se tedy jen kód změněných snippetů, což šetří šířku pásma a zrychluje načítání oproti přenášení obsahu celé stránky. + + Naja -==== +---- -K obsluze AJAXových požadavků na straně prohlížeče slouží [knihovna Naja |https://naja.js.org]. Tu [nainstalujte |https://naja.js.org/#/guide/01-install-setup-naja] jako node.js balíček (pro použití s aplikacemi Webpack, Rollup, Vite, Parcel a dalšími): +K obsluze snippetů na straně prohlížeče slouží [knihovna Naja |https://naja.js.org]. Tu [nainstalujte |https://naja.js.org/#/guide/01-install-setup-naja] jako node.js balíček (pro použití s aplikacemi Webpack, Rollup, Vite, Parcel a dalšími): ```shell npm install naja @@ -64,6 +77,12 @@ npm install naja ``` +Nejprve je potřeba knihovnu [inicializovat |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: + +```js +naja.initialize(); +``` + Aby se z obyčejného odkazu (signálu) nebo odeslání formuláře vytvořil AJAXový požadavek, stačí označit příslušný odkaz, formulář nebo tlačítko třídou `ajax`: ```html @@ -74,64 +93,39 @@ Aby se z obyčejného odkazu (signálu) nebo odeslání formuláře vytvořil AJ nebo +
    ``` -Snippety -======== - -Daleko silnější nástroj představuje vestavěná podpora AJAXových snippetů. Díky ní lze udělat z obyčejné aplikace AJAXovou prakticky několika řádky kódu. Jak to celé funguje, demonstruje příklad Fifteen, jehož kód najdete na [GitHubu |https://github.com/nette-examples/fifteen]. - -Snippety fungují tak, že při prvotním (tedy neAJAXovém) požadavku se přenese celá stránka a poté se při každém již AJAXovém [subrequestu |components#Signál] (= požadavku na stejný presenter a view) přenáší pouze kód změněných částí ve zmíněném úložišti `payload`. K tomu slouží dva mechanismy: invalidace a renderování snippetů. +Překreslení snippetů +-------------------- -Snippety vám mohou připomínat Hotwire pro Ruby on Rails nebo Symfony UX Turbo, nicméně Nette s nimi přišlo už o čtrnáct let dříve. - - -Invalidace snippetů -=================== - -Každý objekt třídy [Control |components] (což je i samotný Presenter) si umí zapamatovat, jestli při signálu došlo ke změnám, které si vyžadují jej překreslit. K tomu slouží dvojice metod `redrawControl()` a `isControlInvalid()`. Příklad: +Každý objekt třídy [Control |components] (včetně samotného Presenteru) eviduje, zda došlo ke změnám vyžadujícím jeho překreslení. K tomu slouží metoda `redrawControl()`: ```php public function handleLogin(string $user): void { - // po přihlášení uživatele se musí objekt překreslit + // po přihlášení je potřeba překreslit relevantní část $this->redrawControl(); // ... } ``` -Nette však nabízí ještě jemnější rozlišení, než na úrovni komponent. Uvedené metody mohou totiž jako argument přijímat název tzv. "snippetu", nebo-li výstřižku. Lze tedy invalidovat (rozuměj: vynutit překreslení) na úrovni těchto snippetů (každý objekt může mít libovolné množství snippetů). Pokud se invaliduje celá komponenta, tak se i každý snippet překreslí. Komponenta je "invalidní" i tehdy, pokud je invalidní některá její subkomponenta. - -```php -$this->isControlInvalid(); // -> false -$this->redrawControl('header'); // invaliduje snippet 'header' -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, alespoň jeden snippet je invalid +Nette umožňuje ještě jemnější kontrolu toho, co se má překreslit. Uvedená metoda totiž může jako argument přijímat název snippetu. Lze tedy invalidovat (rozuměj: vynutit překreslení) na úrovni částí šablony. Pokud se invaliduje celá komponenta, tak se překreslí i každý její snippet: -$this->redrawControl(); // invaliduje celou komponentu, každý snippet -$this->isControlInvalid('footer'); // -> true +```php +// invaliduje snippet 'header' +$this->redrawControl('header'); ``` -Komponenta, která přijímá signál, je automaticky označena za invalidní. - -Díky invalidaci snippetů přesně víme, které části kterých prvků bude potřeba překreslit. - - -Tagy `{snippet} … {/snippet}` .{toc: Tag snippet} -================================================= -Vykreslování stránky probíhá velmi podobně jako při běžném požadavku: načtou se stejné šablony atd. Podstatné však je vynechání částí, které se nemají dostat na výstup; ostatní části se přiřadí k identifikátoru a pošlou se uživateli ve formátu srozumitelném pro obslužný program JavaScriptu. +Snippety v Latte +---------------- - -Syntaxe -------- - -Pokud se uvnitř šablony nachází control nebo snippet, musíme jej obalit párovou značkou `{snippet} ... {/snippet}` - ty totiž zajistí, že se vykreslený snippet vystřihne a pošle do prohlížeče. Také jej obalí pomocnou značkou `
    ` s vygenerovaným `id`. V uvedeném příkladě je snippet pojmenován jako `header` a může představovat i například šablonu controlu: +Používání snippetů v Latte je nesmírně snadné. Chcete-li definovat část šablony jako snippet, obalte ji jednoduše značkami `{snippet}` a `{/snippet}`: ```latte {snippet header} @@ -139,7 +133,9 @@ Pokud se uvnitř šablony nachází control nebo snippet, musíme jej obalit pá {/snippet} ``` -Snippetu jiného typu než `
    ` nebo snippetu s dalšími HTML atributy docílíme použitím atributové varianty: +Snippet vytvoří v HTML stránce element `
    ` se speciálním vygenerovaným `id`. Při překreslení snippetu se pak aktulizuje obsah tohoto elementu. Proto je nutné, aby při prvotním vykreslení stránky se vykreslily také všechny snippety, byť mohou být třeba na začátku prázdné. + +Můžete vytvořit i snippet s jiným elementem než `
    ` pomocí n:attributu: ```latte
    @@ -148,138 +144,106 @@ Snippetu jiného typu než `
    ` nebo snippetu s dalšími HTML atributy docí ``` -Dynamické snippety -================== +Oblasti snippetů +---------------- -Nette také umožňuje používání snippetů, jejichž název se vytvoří až za běhu - tj. dynamicky. Hodí se to pro různé seznamy, kde při změně jednoho řádku nechceme přenášet AJAXem celý seznam, ale stačí onen samotný řádek. Příklad: +Názvy snippetů mohou být také výrazy: ```latte -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Zde máme statický snippet `itemsContainer`, obsahující několik dynamických snippetů `item-0`, `item-1` atd. +Takto nám vznikne několik snippetů `item-0`, `item-1` atd. Pokud bychom přímo invalidovali dynamický snippet (například `item-1`), nepřekreslilo by se nic. Důvod je ten, že snippety opravdu fungují jako výstřižky a vykreslují se jen přímo ony samotné. Jenže v šabloně fakticky žádný snippet pojmenovaný `item-1` není. Ten vznikne až vykonáváním kódu v okolí snippetu, tedy cyklu foreach. Označíme proto část šablony, která se má vykonat pomocí značky `{snippetArea}`: -Dynamické snippety nelze invalidovat přímo (invalidace `item-1` neudělá vůbec nic), musíte invalidovat jim nadřazený statický snippet (zde snippet `itemsContainer`). Potom dojde k tomu, že se provede celý kód toho kontejneru, ale prohlížeči se pošlou jenom jeho sub-snippety. Pokud chcete, aby prohlížeč dostal pouze jediný z nich, musíte upravit vstup toho kontejneru tak, aby ostatní negeneroval. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -V příkladu výše zkrátka musíte zajistit, aby při ajaxovém požadavku byla v proměnné `$list` pouze jedna položka a tedy aby ten cyklus `foreach` naplnil pouze jeden dynamický snippet: +A necháme překreslit jak samotný snippet, tak i celou nadřazenou oblast: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * Tato metoda vrací data pro seznam. - * Obvykle se jedná pouze o vyžádání dat z modelu. - * Pro účely tohoto příkladu jsou data zadána natvrdo. - */ - private function getTheWholeList(): array - { - return [ - 'První', - 'Druhý', - 'Třetí', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +Zároveň je vhodné zajistit, aby pole `$items` obsahovalo jen ty položky, které se mají překreslit. -Snippety v includované šabloně -============================== - -Může se stát, že máme snippet v šabloně, kterou teprve includujeme do jiné šablony. V takovém případě je nutné vkládání této šablony obalit značkami `snippetArea`, které pak invalidujeme spolu se samotnym snippetem. - -Tagy `snippetArea` zaručí, že se daný kód, který vkládá šablonu, provede, do prohlížeče se však odešle pouze snippet v includované šabloně. +Pokud do šablony vkládáme pomocí značky `{include}` jinou šablonu, která obsahuje snippety, je nutné vložení šablony opět zahrnout do `snippetArea` a tu invalidovat společně se snippetem: ```latte -{* parent.latte *} -{snippetArea wrapper} -{include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* included.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -Tento přístup se nechá použít i v kombinaci s dynamickými snippety. +Snippety v komponentách +----------------------- -Přidávání a mazání -================== - -Pokud přidáte novou položku a invalidujete `itemsContainer`, pak vám AJAXový požadavek sice vrátí i nový snippet, ale obslužný javascript ho neumí nikam přiřadit. Na stránce totiž zatím není žádný HTML prvek s takovým ID. - -V takovém případě je nejjednodušší celý ten seznam obalit ještě jedním snippetem a invalidovat to celé: +Snippety můžete vytvářet i v [komponentách|components] a Nette je bude automaticky překreslovat. Ale platí tu určité omezení: pro překreslení snippetů volá metodu `render()` bez parametrů. Tedy nebude fungovat předávání parametrů v šabloně: ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +nebude fungovat: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Posílání uživatelských dat +-------------------------- + +Společně se snippety můžete klientovi poslat libovolná další data. Stačí je zapsat do objektu `payload`: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + // ... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -Totéž platí i pro mazání. Sice by se dal nějak poslat prázdný snippet, jenže v praxi jsou většinou seznamy stránkované a řešit úsporněji smazání jednoho plus případné načtení jiného (který se předtím nevešel) by bylo příliš složité. - -Posílání parametrů do komponenty -================================ +Předávání parametrů +=================== Pokud komponentě pomocí AJAXového požadavku odesíláme parametry, ať už parametry signálu nebo persistentní parametry, musíme u požadavku uvést jejich globální název, který obsahuje i jméno komponenty. Celý název parametru vrací metoda `getParameterId()`. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -A handle metoda s odpovídajícími parametry v komponentě. +A handle metoda s odpovídajícími parametry v komponentě: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/cs/bootstrap.texy b/application/cs/bootstrap.texy index 55fbbfc0f5..5cf576a195 100644 --- a/application/cs/bootstrap.texy +++ b/application/cs/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + private Configurator $configurator; + private string $rootDir; + + public function __construct() + { + $this->rootDir = dirname(__DIR__); + // Konfigurátor je zodpovědný za nastavení prostředí aplikace a služeb. + $this->configurator = new Configurator; + // Nastaví adresář pro dočasné soubory generované Nette (např. zkompilované šablony) + $this->configurator->setTempDirectory($this->rootDir . '/temp'); + } + + public function bootWebApplication(): Nette\DI\Container + { + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); + } + + private function initializeEnvironment(): void { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // Nette je chytré a vývojový režim se zapíná automaticky, + // nebo jej můžete povolit pro konkrétní IP adresu odkomentováním následujícího řádku: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Aktivuje Tracy: ultimátní "švýcarský nůž" pro ladění. + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: automaticky načítá všechny třídy ve zvoleném adresáři + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Načte konfigurační soubory + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php ========= -Prvotní soubor je v případě webových aplikací `index.php`, který se nachází ve veřejném adresáři `www/`. Ten si nechá od třídy Bootstrap inicializovat prostředí a vrátit `$configurator` a následně vyrobí DI kontejner. Poté z něj získá službu `Application`, kterou spustí webovou aplikaci: +Prvotní soubor je v případě webových aplikací `index.php`, který se nachází ve [veřejném adresáři |directory-structure#verejny-adresar-www] `www/`. Ten si nechá od třídy Bootstrap inicializovat prostředí a vyrobit DI kontejner. Poté z něj získá službu `Application`, která spustí webovou aplikaci: ```php -// inicializace prostředí + získání objektu Configurator -$configurator = App\Bootstrap::boot(); -// vytvoření DI kontejneru -$container = $configurator->createContainer(); +$bootstrap = new App\Bootstrap; +// Inicializace prostředí + vytvoření DI kontejneru +$container = $bootstrap->bootWebApplication(); // DI kontejner vytvoří objekt Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -// spuštění Nette aplikace +// Spuštění aplikace Nette a zpracování příchozího požadavku $application->run(); ``` @@ -59,26 +84,42 @@ Jak vidno, s nastavením prostředí a vytvořením dependency injection (DI) ko Vývojářský vs produkční režim ============================= -Nette rozlišuje dva základní režimy, ve kterých se požadavek vykoná: vývojářský a produkční. Vývojářský je zaměřen na maximální pohodlí programátora, zobrazuje se Tracy, automaticky se aktualizuje cache při změně šablon nebo konfigurace DI kontejneru, atd. Produkční je zaměřený na výkon a ostré nasazení, Tracy chyby pouze loguje a změny šablon a dalších souborů se netestují. +Nette se chová různě podle toho, zda běží na vývojářském nebo produkčním serveru: + +🛠️ Vývojářský režim (Development): + - Zobrazuje Tracy debugbar s užitečnými informacemi (SQL dotazy, čas vykonání, použitá paměť) + - Při chybě zobrazí detailní chybovou stránku s voláním funkcí a obsahem proměnných + - Automaticky obnovuje cache při změně Latte šablon, úpravě konfiguračních souborů atd. + + +🚀 Produkční režim (Production): + - Nezobrazuje žádné ladící informace, všechny chyby zapisuje do logu + - Při chybě zobrazí ErrorPresenter nebo obecnou stránku "Server Error" + - Cache se nikdy automaticky neobnovuje! + - Optimalizovaný pro rychlost a bezpečnost -Volba režimu se provádí autodetekcí, takže obvykle není potřeba nic konfigurovat nebo ručně přepínat. Režim je vývojářský tehdy, pokud je aplikace spuštěna na localhostu (tj. IP adresa `127.0.0.1` nebo `::1`) a není přitomna proxy (tj. její HTTP hlavička). Jinak běží v produkčním režimu. + +Volba režimu se provádí autodetekcí, takže obvykle není potřeba nic konfigurovat nebo ručně přepínat: + +- vývojářský režim: na localhostu (IP adresa `127.0.0.1` nebo `::1`) pokud není přítomná proxy (tj. její HTTP hlavička) +- produkční režim: všude jinde Pokud chceme vývojářský režim povolit i v dalších případech, například programátorům přistupujícím z konkrétní IP adresy, použijeme `setDebugMode()`: ```php -$configurator->setDebugMode('23.75.345.200'); // lze uvést i pole IP adres +$this->configurator->setDebugMode('23.75.345.200'); // lze uvést i pole IP adres ``` Rozhodně doporučujeme kombinovat IP adresu s cookie. Do cookie `nette-debug` uložíme tajný token, např. `secret1234`, a tímto způsobem aktivujeme vývojářský režim pro programátory přistupující z konkrétní IP adresy a zároveň mající v cookie zmíněný token: ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` Vývojářský režim můžeme také vypnout úplně, i pro localhost: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` Pozor, hodnota `true` zapne vývojářský režim natvrdo, což se nikdy nesmí stát na produkčním serveru. @@ -90,7 +131,7 @@ Debugovací nástroj Tracy Pro snadné debugování ještě zapneme skvělý nástroj [Tracy |tracy:]. Ve vývojářském režimu chyby vizualizuje a v produkčním režimu chyby loguje do uvedeného adresáře: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ Dočasné soubory Nette využívá cache pro DI kontejner, RobotLoader, šablony atd. Proto je nutné nastavit cestu k adresáři, kam se bude cache ukládat: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` Na Linuxu nebo macOS nastavte adresářům `log/` a `temp/` [práva pro zápis |nette:troubleshooting#Nastavení práv adresářů]. @@ -112,7 +153,7 @@ RobotLoader Zpravidla budeme chtít automaticky načítat třídy pomocí [RobotLoaderu |robot-loader:], musíme ho tedy nastartovat a necháme jej načítat třídy z adresáře, kde je umístěný `Bootstrap.php` (tj. `__DIR__`), a všech podadresářů: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ Timezone Přes konfigurátor můžete nastavit výchozí časovou zónu. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ Ve vývojářském režimu se kontejner automaticky aktualizuje při každé zm Konfigurační soubory načteme pomocí `addConfig()`: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` Pokud chceme přidat více konfiguračních souborů, můžeme funkci `addConfig()` zavolat vícekrát. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ Statické parametry Parametry používané v konfiguračních souborech můžeme definovat [v sekci `parameters`|dependency-injection:configuration#parametry] a také je předávat (či přepisovat) metodou `addStaticParameters()` (má alias `addParameters()`). Důležité je, že různé hodnoty parametrů způsobí vygenerování dalších DI kontejnerů, tedy dalších tříd. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ Dynamické parametry Do kontejneru můžeme přidat i dynamické parametry, jejichž různé hodnoty na rozdíl od statických parameterů nezpůsobí generování nových DI kontejnerů. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ Jednoduše tak můžeme přidat např. environmentální proměnné, na které se pak lze v konfiguraci odkázat zápisem `%env.variable%`. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ V konfiguračních souborech můžete využít tyto statické parametry: - `%wwwDir%` je absolutní cesta k adresáři se vstupním souborem `index.php` - `%tempDir%` je absolutní cesta k adresáři pro dočasné soubory - `%vendorDir%` je absolutní cesta k adresáři, kam Composer instaluje knihovny +- `%rootDir%` je absolutní cesta ke kořenovému adresáři projektu - `%debugMode%` udává, zda je aplikace v debugovacím režimu - `%consoleMode%` udává, zda request přišel přes příkazovou řádku @@ -225,7 +268,7 @@ services: A v bootstrapu do kontejneru vložíme objekt: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Odlišné prostředí ================= -Nebojte se upravit třídu Bootstrap podle svých potřeb. Metodě `boot()` můžete přidat parametry pro rozlišení webových projektů nebo doplnit další metody, například `bootForTests()`, která inicializuje prostředí pro jednotkové testy, `bootForCli()` pro skripty volané z příkazové řádky atd. +Nebojte se upravit třídu Bootstrap podle svých potřeb. Metodě `bootWebApplication()` můžete přidat parametry pro rozlišení webových projektů. Nebo můžeme doplnit další metody, například `bootTestEnvironment()`, která inicializuje prostředí pro jednotkové testy, `bootConsoleApplication()` pro skripty volané z příkazové řádky atd. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container { - $configurator = self::boot(); Tester\Environment::setup(); // inicializace Nette Testeru - return $configurator; + $this->setupContainer(); + return $this->configurator->createContainer(); +} + +public function bootConsoleApplication(): Nette\DI\Container +{ + $this->configurator->setDebugMode(false); + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); } ``` diff --git a/application/cs/components.texy b/application/cs/components.texy index c7ca3c65ef..db86af248c 100644 --- a/application/cs/components.texy +++ b/application/cs/components.texy @@ -198,7 +198,7 @@ Odkaz, který zavolá signál, vytvoříme obvyklým způsobem, tedy v šabloně click here ``` -Signál se vždy volá na aktuálním presenteru a view, tudíž není možné jej vyvolat na jiném presenteru nebo view. +Signál se vždy volá na aktuálním presenteru a action, není možné jej vyvolat na jiném presenteru nebo jiné action. Signál tedy způsobí znovunačtení stránky úplně stejně jako při původním požadavku, jen navíc zavolá obslužnou metodu signálu s příslušnými parametry. Pokud metoda neexistuje, vyhodí se výjimka [api:Nette\Application\UI\BadSignalException], která se uživateli zobrazí jako chybová stránka 403 Forbidden. @@ -230,6 +230,28 @@ $this->redirect(/* ... */); // a přesměrujeme ``` +Přesměrování po signálu +======================= + +Po zpracování signálu komponenty často následuje přesměrování. Je to podobná situace jako u formulářů - po jejich odeslání také přesměrováváme, aby při obnovení stránky v prohlížeči nedošlo k opětovnému odeslání dat. + +```php +$this->redirect('this') // přesměruje na aktuální presenter a action +``` + +Protože komponenta je znovupoužitelný prvek a obvykle by neměla mít přímou vazbu na konkrétní presentery, metody `redirect()` a `link()` automaticky interpretují parametr jako signál komponenty: + +```php +$this->redirect('click') // přesměruje na signál 'click' téže komponenty +``` + +Pokud potřebujete přesměrovat na jiný presenter či akci, můžete to udělat prostřednictvím presenteru: + +```php +$this->getPresenter()->redirect('Product:show'); // přesměruje na jiný presenter/action +``` + + Persistentní parametry ====================== @@ -347,7 +369,7 @@ services: a nakonec ji použijeme v našem presenteru: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ Komponenty do hloubky Komponenty v Nette Application představují znovupoužitelné součásti webové aplikace, které vkládáme do stránek a kterým se ostatně věnuje celá tato kapitola. Jaké přesně schopnosti taková komponenta má? 1) je vykreslitelná v šabloně -2) ví, kterou svou část má vykreslit při [AJAXovém požadavku |ajax#invalidace] (snippety) +2) ví, [kterou svou část|ajax#snippety] má vykreslit při AJAXovém požadavku (snippety) 3) má schopnost ukládat svůj stav do URL (persistentní parametry) 4) má schopnost reagovat na uživatelské akce (signály) 5) vytváří hierarchickou strukturu (kde kořenem je presenter) @@ -430,7 +452,7 @@ class PaginatingControl extends Control } ``` -Opačný proces, tedy sesbírání hodnot z persistentních properites, má na starosti metoda `saveState()`. +Opačný proces, tedy sesbírání hodnot z persistentních properties, má na starosti metoda `saveState()`. Signály do hloubky @@ -444,7 +466,7 @@ Signál může přijímat jakákoliv komponenta, presenter nebo objekt, který i Mezi hlavní příjemce signálů budou patřit `Presentery` a vizuální komponenty dědící od `Control`. Signál má sloužit jako znamení pro objekt, že má něco udělat – anketa si má započítat hlas od uživatele, blok s novinkami se má rozbalit a zobrazit dvakrát tolik novinek, formulář byl odeslán a má zpracovat data a podobně. -URL pro signál vytváříme pomocí metody [Component::link() |api:Nette\Application\UI\Component::link()]. Jako parametr `$destination` předáme řetězec `{signal}!` a jako `$args` pole argumentů, které chceme signálu předat. Signál se vždy volá na aktuální view s aktuálními parametry, parametry signálu se jen přidají. Navíc se přidává hned na začátku **parametr `?do`, který určuje signál**. +URL pro signál vytváříme pomocí metody [Component::link() |api:Nette\Application\UI\Component::link()]. Jako parametr `$destination` předáme řetězec `{signal}!` a jako `$args` pole argumentů, které chceme signálu předat. Signál se vždy volá na aktuálním presenteru a action s aktuálními parametry, parametry signálu se jen přidají. Navíc se přidává hned na začátku **parametr `?do`, který určuje signál**. Jeho formát je buď `{signal}`, nebo `{signalReceiver}-{signal}`. `{signalReceiver}` je název komponenty v presenteru. Proto nemůže být v názvu komponenty pomlčka – používá se k oddělení názvu komponenty a signálu, je ovšem možné takto zanořit několik komponent. diff --git a/application/cs/configuration.texy b/application/cs/configuration.texy index 6f258ecc63..96aae17dfc 100644 --- a/application/cs/configuration.texy +++ b/application/cs/configuration.texy @@ -14,10 +14,14 @@ application: debugger: ... # (bool) výchozí je true # bude se při chybě volat error-presenter? - catchExceptions: ... # (bool) výchozí je true v produkčním režimu + # má efekt pouze ve vývojářském režimu + catchExceptions: ... # (bool) výchozí je true # název error-presenteru - errorPresenter: Error # (string) výchozí je 'Nette:Error' + errorPresenter: Error # (string|array) výchozí je 'Nette:Error' + + # definuje aliasy pro presentery a akce + aliases: ... # definuje pravidla pro překlad názvu presenteru na třídu mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) výchozí je false ``` -Protože ve vývojovém režimu se error-presentery standardně nevolají a chybu zobrazí až Tracy, změnou hodnoty `catchExceptions` na `true` můžeme při vývoji ověřit jejich správnou funkčnost. +Od `nette/application` verze 3.2 lze definovat dvojici error-presenterů: + +```neon +application: + errorPresenter: + 4xx: Error4xx # pro výjimku Nette\Application\BadRequestException + 5xx: Error5xx # pro ostatní výjimky +``` Volba `silentLinks` určuje, jak se Nette zachová ve vývojářském režimu, když selže generování odkazu (třeba proto, že neexistuje presenter, atd). Výchozí hodnota `false` znamená, že Nette vyhodí `E_USER_WARNING` chybu. Nastavením na `true` dojde k potlačení této chybové hlášky. V produkčním prostředí se `E_USER_WARNING` vyvolá vždy. Toto chování můžeme také ovlivnit nastavením proměnné presenteru [$invalidLinkMode|creating-links#neplatne-odkazy]. -[Mapování definuje pravidla |modules#mapování], podle kterých se z názvu presenteru odvodí název třídy. +[Aliasy zjednodušují odkazování |creating-links#aliasy] na často používané presentery. + +[Mapování definuje pravidla |directory-structure#mapování presenterů], podle kterých se z názvu presenteru odvodí název třídy. Automatická registrace presenterů @@ -82,6 +95,9 @@ latte: # aktivuje [kontrolu vygenerovaného kódu |latte:develop#Kontrola vygenerovaného kódu] phpLinter: ... # (string) výchozí je null + # nastaví locale + locale: cs_CZ # (string) výchozí je null + # třída objektu $this->template templateClass: App\MyTemplateClass # výchozí je Nette\Bridges\ApplicationLatte\DefaultTemplate ``` @@ -91,7 +107,7 @@ Pokud používáte Latte verze 3, můžete přidávat nové [rozšíření |latt ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` Pokud používáte Latte verze 2, můžete registrovat nové tagy (makra) buď uvedením jména třídy, nebo referencí na službu. Jako výchozí je zavolána metoda `install()`, ale to lze změnit tím, že uvedeme jméno jiné metody: diff --git a/application/cs/creating-links.texy b/application/cs/creating-links.texy index a21c986098..45f53eb532 100644 --- a/application/cs/creating-links.texy +++ b/application/cs/creating-links.texy @@ -38,7 +38,7 @@ Je možné předávat i pojmenované parametry. Následující odkaz předává detail produktu ``` -Pokud metoda `ProductPresenter::renderShow()` nemá `$lang` ve své signatuře, může si hodnotu parametru zjistit pomocí `$lang = $this->getParameter('lang')`. +Pokud metoda `ProductPresenter::renderShow()` nemá `$lang` ve své signatuře, může si hodnotu parametru zjistit pomocí `$lang = $this->getParameter('lang')` nebo z [property |presenters#Parametry požadavku]. Pokud jsou parametry uložené v poli, lze je rozvinout operátorem `...` (v Latte 2.x operátorem `(expand)`): @@ -103,7 +103,7 @@ Pokud je cílem akce `default`, můžeme ji vynechat, ale dvojtečka musí zůst úvodní stránka ``` -Odkazy mohou také směřovat do jiných [modulů |modules]. Zde se odkazy rozlišují na relativní do zanořeného submodulu, nebo absolutní. Princip je analogický k cestám na disku, jen místo lomítek jsou dvojtečky. Předpokládejme, že aktuální presenter je součástí modulu `Front`, potom zapíšeme: +Odkazy mohou také směřovat do jiných [modulů |directory-structure#Presentery a šablony]. Zde se odkazy rozlišují na relativní do zanořeného submodulu, nebo absolutní. Princip je analogický k cestám na disku, jen místo lomítek jsou dvojtečky. Předpokládejme, že aktuální presenter je součástí modulu `Front`, potom zapíšeme: ```latte odkaz na Front:Shop:Product:show @@ -140,7 +140,7 @@ Cíl `this` vytvoří odkaz na aktuální stránku: refresh ``` -Zároveň se přenáší i všechny parametry uvedené v signatuře `render()` nebo `action()` metody. Takže pokud jsme na stránce `Product:show` a `id: 123`, odkaz na `this` předá i tento parameter. +Zároveň se přenáší i všechny parametry uvedené v signatuře metody `action()` nebo `render()`, pokud není `action()` definovaná. Takže pokud jsme na stránce `Product:show` a `id: 123`, odkaz na `this` předá i tento parameter. Samozřejmě je možné parametry specifikovat přímo: @@ -213,7 +213,7 @@ Protože [komponenty|components] jsou samostatné znovupoužitelné celky, kter Pokud bychom chtěli v šabloně komponenty odkazovat na presentery, použijeme k tomu značku `{plink}`: ```latte -úvod +úvod ``` nebo v kódu @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Aliasy .{data-version:v3.2.2} +============================= + +Občas se může hodit přiřadit dvojici Presenter:akce snadno zapamatovatelný alias. Například úvodní stránku `Front:Home:default` pojmenovat jednoduše jako `home` nebo `Admin:Dashboard:default` jako `admin`. + +Aliasy se definují v [konfiguraci|configuration] pod klíčem `application › aliases`: + +```neon +application: + aliases: + home: Front:Home:default + admin: Admin:Dashboard:default + sign: Front:Sign:in +``` + +V odkazech se pak zapisují pomocí zavináče, například: + +```latte +administrace +``` + +Podporované jsou i ve všech metodách pracujících s odkazy, jako je `redirect()` a podobně. + + Neplatné odkazy =============== @@ -257,6 +281,6 @@ Jak vytvářet odkazy s podobným komfortem jako má metoda `link()`, ale bez p LinkGenerátor je služba, kterou si můžete nechat předat přes konstruktor a poté vytvářet odkazy jeho metodou `link()`. -Oproti presenterům je tu rozdíl. LinkGenerator vytváří všechny odkazy rovnou jako absolutní URL. A dále neexistuje žádný "aktuální presenter", takže nelze jako cíl uvést jen název akce `link('default')` nebo uvádět relativní cesty k [modulům |modules]. +Oproti presenterům je tu rozdíl. LinkGenerator vytváří všechny odkazy rovnou jako absolutní URL. A dále neexistuje žádný "aktuální presenter", takže nelze jako cíl uvést jen název akce `link('default')` nebo uvádět relativní cesty k modulům. Neplatné odkazy vždy vyhazují `Nette\Application\UI\InvalidLinkException`. diff --git a/application/cs/directory-structure.texy b/application/cs/directory-structure.texy new file mode 100644 index 0000000000..ca6d16313e --- /dev/null +++ b/application/cs/directory-structure.texy @@ -0,0 +1,526 @@ +Adresářová struktura aplikace +***************************** + +
    + +Jak navrhnout přehlednou a škálovatelnou adresářovou strukturu pro projekty v Nette Framework? Ukážeme si osvědčené postupy, které vám pomohou s organizací kódu. Dozvíte se: + +- jak **logicky rozčlenit** aplikaci do adresářů +- jak strukturu navrhnout tak, aby **dobře škálovala** s růstem projektu +- jaké jsou **možné alternativy** a jejich výhody či nevýhody + +
    + + +Důležité je zmínit, že Nette Framework samotný na žádné konkrétní struktuře nelpí. Je navržen tak, aby se dal snadno přizpůsobit jakýmkoliv potřebám a preferencím. + + +Základní struktura projektu +=========================== + +Přestože Nette Framework nediktuje žádnou pevnou adresářovou strukturu, existuje osvědčené výchozí uspořádání v podobě [Web Project|https://github.com/nette/web-project]: + +/--pre +web-project/ +├── app/ ← adresář s aplikací +├── assets/ ← soubory SCSS, JS, obrázky..., alternativně resources/ +├── bin/ ← skripty pro příkazovou řádku +├── config/ ← konfigurace +├── log/ ← logované chyby +├── temp/ ← dočasné soubory, cache +├── tests/ ← testy +├── vendor/ ← knihovny instalované Composerem +└── www/ ← veřejný adresář (document-root) +\-- + +Tuto strukturu můžete libovolně upravovat podle svých potřeb - složky přejmenovat či přesouvat. Poté stačí pouze upravit relativní cesty k adresářům v souboru `Bootstrap.php` a případně `composer.json`. Nic víc není potřeba, žádná složitá rekonfigurace, žádné změny konstant. Nette disponuje chytrou autodetekcí a automaticky rozpozná umístění aplikace včetně její URL základny. + + +Principy organizace kódu +======================== + +Když poprví prozkoumáváte nový projekt, měli byste se v něm rychle zorientovat. Představte si, že rozkliknete adresář `app/Model/` a uvidíte tuto strukturu: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +Z ní vyčtete jen to, že projekt používá nějaké služby, repozitáře a entity. O skutečném účelu aplikace se nedozvíte vůbec nic. + +Podívejme se na jiný přístup - **organizaci podle domén**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Tady je to jiné - na první pohled je jasné, že jde o e-shop. Už samotné názvy adresářů prozrazují, co aplikace umí - pracuje s platbami, objednávkami a produkty. + +První přístup (organizace podle typu tříd) přináší v praxi řadu problémů: kód, který spolu logicky souvisí, je roztříštěný do různých složek a musíte mezi nimi přeskakovat. Proto budeme organizovat podle domén. + + +Jmenné prostory +--------------- + +Je zvykem, že adresářová struktura koresponduje se jmennými prostory v aplikaci. To znamená, že fyzické umístění souborů odpovídá jejich namespace. Například třída umístěná v `app/Model/Product/ProductRepository.php` by měla mít namespace `App\Model\Product`. Tento princip pomáhá v orientaci v kódu a zjednodušuje autoloading. + + +Jednotné vs množné číslo v názvech +---------------------------------- + +Všimněte si, že u hlavních adresářů aplikace používáme jednotné číslo: `app`, `config`, `log`, `temp`, `www`. Stejně tak i uvnitř aplikace: `Model`, `Core`, `Presentation`. Je to proto, že každý z nich představuje jeden ucelený koncept. + +Podobně třeba `app/Model/Product` reprezentuje vše kolem produktů. Nenazveme to `Products`, protože nejde o složku plnou produktů (to by tam byly soubory `nokia.php`, `samsung.php`). Je to namespace obsahující třídy pro práci s produkty - `ProductRepository.php`, `ProductService.php`. + +Složka `app/Tasks` je v množném čísle proto, že obsahuje sadu samostatných spustitelných skriptů - `CleanupTask.php`, `ImportTask.php`. Každý z nich je samostatnou jednotkou. + +Pro konzistenci doporučujeme používat: +- Jednotné číslo pro namespace reprezentující funkční celek (byť pracující s více entitami) +- Množné číslo pro kolekce samostatných jednotek +- V případě nejistoty nebo pokud nad tím nechcete přemýšlet, zvolte jednotné číslo + + +Veřejný adresář `www/` +====================== + +Tento adresář je jediný přístupný z webu (tzv. document-root). Často se můžete setkat i s názvem `public/` místo `www/` - je to jen otázka konvence a na funkčnost rostlináře to nemá vliv. Adresář obsahuje: +- [Vstupní bod |bootstrap#index.php] aplikace `index.php` +- Soubor `.htaccess` s pravidly pro mod_rewrite (u Apache) +- Statické soubory (CSS, JavaScript, obrázky) +- Uploadované soubory + +Pro správné zabezpečení aplikace je zásadní mít správně [nakonfigurovaný document-root |nette:troubleshooting#Jak změnit či ostranit z URL adresář www]. + +.[note] +Nikdy neumisťujte do tohoto adresáře složku `node_modules/` - obsahuje tisíce souborů, které mohou být spustitelné a neměly by být veřejně dostupné. + + +Aplikační adresář `app/` +======================== + +Toto je hlavní adresář s aplikačním kódem. Základní struktura: + +/--pre +app/ +├── Core/ ← infrastrukturní záležitosti +├── Model/ ← business logika +├── Presentation/ ← presentery a šablony +├── Tasks/ ← příkazové skripty +└── Bootstrap.php ← zaváděcí třída aplikace +\-- + +`Bootstrap.php` je [startovací třída aplikace|bootstrap], která inicializuje prostředí, načítá konfiguraci a vytváří DI kontejner. + +Pojďme se nyní podívat na jednotlivé podadresáře podrobněji. + + +Presentery a šablony +==================== + +Prezentační část aplikace máme v adresáři `app/Presentation`. Alternativou je krátké `app/UI`. Je to místo pro všechny presentery, jejich šablony a případné pomocné třídy. + +Tuto vrstvu organizujeme podle domén. V komplexním projektu, který kombinuje e-shop, blog a API, by struktura vypadala takto: + +/--pre +app/Presentation/ +├── Shop/ ← e-shop frontend +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← blog +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administrace +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← API endpointy + └── V1/ +\-- + +Naopak u jednoduchého blogu bychom použili členění: + +/--pre +app/Presentation/ +├── Front/ ← frontend webu +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administrace +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, sitemapy atd. +\-- + +Složky jako `Home/` nebo `Dashboard/` obsahují presentery a šablony. Složky jako `Front/`, `Admin/` nebo `Api/` nazýváme **moduly**. Technicky jde o běžné adresáře, které slouží k logickému členění aplikace. + +Každá složka s presenterem obsahuje stejně pojmenovaný presenter a jeho šablony. Například složka `Dashboard/` obsahuje: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← presenter +└── default.latte ← šablona +\-- + +Tato adresářová struktura se odráží ve jmenných prostorech tříd. Například `DashboardPresenter` se nachází ve jmenném prostoru `App\Presentation\Admin\Dashboard` (viz [#mapování presenterů]): + +```php +namespace App\Presentation\Admin\Dashboard; + +class DashboardPresenter extends Nette\Application\UI\Presenter +{ + // ... +} +``` + +Na presenter `Dashboard` uvnitř modulu `Admin` odkazujeme v aplikaci pomocí dvojtečkové notace jako na `Admin:Dashboard`. Na jeho akci `default` potom jako na `Admin:Dashboard:default`. V případě zanořených modulů používáme více dvojteček, například `Shop:Order:Detail:default`. + + +Flexibilní vývoj struktury +-------------------------- + +Jednou z velkých výhod této struktury je, jak elegantně se přizpůsobuje rostoucím potřebám projektu. Jako příklad si vezměme část generující XML feedy. Na začátku máme jednoduchou podobu: + +/--pre +Export/ +├── ExportPresenter.php ← jeden presenter pro všechny exporty +├── sitemap.latte ← šablona pro sitemapu +└── feed.latte ← šablona pro RSS feed +\-- + +Časem přibydou další typy feedů a potřebujeme pro ně více logiky... Žádný problém! Složka `Export/` se jednoduše stane modulem: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── zbozi.latte ← feed pro Zboží.cz + └── heureka.latte ← feed pro Heureka.cz +\-- + +Tato transformace je naprosto plynulá - stačí vytvořit nové podsložky, rozdělit do nich kód a aktualizovat odkazy (např. z `Export:feed` na `Export:Feed:zbozi`). Díky tomu můžeme strukturu postupně rozšiřovat podle potřeby, úroveň zanoření není nijak omezena. + +Pokud například v administraci máte mnoho presenterů týkajících se správy objednávek, jako jsou `OrderDetail`, `OrderEdit`, `OrderDispatch` atd., můžete pro lepší organizovanost v tomto místě vytvořit modul (složku) `Order`, ve kterém budou (složky pro) presentery `Detail`, `Edit`, `Dispatch` a další. + + +Umístění šablon +--------------- + +V předchozích ukázkách jsme viděli, že šablony jsou umístěny přímo ve složce s presenterem: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← presenter +├── DashboardTemplate.php ← volitelná třída pro šablonu +└── default.latte ← šablona +\-- + +Toto umístění se v praxi ukazuje jako nejpohodlnější - všechny související soubory máte hned po ruce. + +Alternativně můžete šablony umístit do podsložky `templates/`. Nette podporuje obě varianty. Dokonce můžete šablony umístit i úplně mimo `Presentation/` složku. Vše o možnostech umístění šablon najdete v kapitole [Hledání šablon|templates#Hledání šablon]. + + +Pomocné třídy a komponenty +-------------------------- + +K prezenterům a šablonám často patří i další pomocné soubory. Umístíme je logicky podle jejich působnosti: + +1. **Přímo u presenteru** v případě specifických komponent pro daný presenter: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← komponenta pro výpis produktů +└── FilterForm.php ← formulář pro filtrování +\-- + +2. **Pro modul** - doporučujeme využít složku `Accessory`, která se umístí přehledně hned na začátku abecedy: + +/--pre +Front/ +├── Accessory/ +│ ├── NavbarControl.php ← komponenty pro frontend +│ └── TemplateFilters.php +├── Product/ +└── Cart/ +\-- + +3. **Pro celou aplikaci** - v `Presentation/Accessory/`: +/--pre +app/Presentation/ +├── Accessory/ +│ ├── LatteExtension.php +│ └── TemplateFilters.php +├── Front/ +└── Admin/ +\-- + +Nebo můžete pomocné třídy jako `LatteExtension.php` nebo `TemplateFilters.php` umístit do infrastrukturní složky `app/Core/Latte/`. A komponenty do `app/Components`. Volba závisí na zvyklostech týmu. + + +Model - srdce aplikace +====================== + +Model obsahuje veškerou business logiku aplikace. Pro jeho organizaci platí opět pravidlo - strukturujeme podle domén: + +/--pre +app/Model/ +├── Payment/ ← vše kolem plateb +│ ├── PaymentFacade.php ← hlavní vstupní bod +│ ├── PaymentRepository.php +│ ├── Payment.php ← entita +├── Order/ ← vše kolem objednávek +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← vše kolem dopravy +\-- + +V modelu se typicky setkáte s těmito typy tříd: + +**Fasády**: představují hlavní vstupní bod do konkrétní domény v aplikaci. Působí jako orchestrátor, který koordinuje spolupráci mezi různými službami za účelem implementace kompletních use-cases (jako "vytvoř objednávku" nebo "zpracuj platbu"). Pod svojí orchestrační vrstvou fasáda skrývá implementační detaily před zbytkem aplikace, čímž poskytuje čisté rozhraní pro práci s danou doménou. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // validace + // vytvoření objednávky + // odeslání e-mailu + // zapsání do statistik + } +} +``` + +**Služby**: zaměřují se na specifickou business operaci v rámci domény. Na rozdíl od fasády, která orchestruje celé use-cases, služba implementuje konkrétní byznys logiku (jako výpočty cen nebo zpracování plateb). Služby jsou typicky bezstavové a mohou být použity buď fasádami jako stavební bloky pro komplexnější operace, nebo přímo jinými částmi aplikace pro jednodušší úkony. + +```php +class PricingService +{ + public function calculateTotal(Order $order): Money + { + // výpočet ceny + } +} +``` + +**Repozitáře**: zajišťují veškerou komunikaci s datovým úložištěm, typicky databází. Jeho úkolem je načítání a ukládání entit a implementace metod pro jejich vyhledávání. Repozitář odstiňuje zbytek aplikace od implementačních detailů databáze a poskytuje objektově orientované rozhraní pro práci s daty. + +```php +class OrderRepository +{ + public function find(int $id): ?Order + { + } + + public function findByCustomer(int $customerId): array + { + } +} +``` + +**Entity**: objekty reprezentující hlavní byznys koncepty v aplikaci, které mají svou identitu a mění se v čase. Typicky jde o třídy mapované na databázové tabulky pomocí ORM (jako Nette Database Explorer nebo Doctrine). Entity mohou obsahovat business pravidla týkající se jejich dat a validační logiku. + +```php +// Entita mapovaná na databázovou tabulku orders +class Order extends Nette\Database\Table\ActiveRow +{ + public function addItem(Product $product, int $quantity): void + { + $this->related('order_items')->insert([ + 'product_id' => $product->id, + 'quantity' => $quantity, + 'unit_price' => $product->price, + ]); + } +} +``` + +**Value objekty**: neměnné objekty reprezentující hodnoty bez vlastní identity - například peněžní částka nebo e-mailová adresa. Dvě instance value objektu se stejnými hodnotami jsou považovány za identické. + + +Infrastrukturní kód +=================== + +Složka `Core/` (nebo také `Infrastructure/`) je domovem pro technický základ aplikace. Infrastrukturní kód typicky zahrnuje: + +/--pre +app/Core/ +├── Router/ ← routování a URL management +│ └── RouterFactory.php +├── Security/ ← autentizace a autorizace +│ ├── Authenticator.php +│ └── Authorizator.php +├── Logging/ ← logování a monitoring +│ ├── SentryLogger.php +│ └── FileLogger.php +├── Cache/ ← cachovací vrstva +│ └── FullPageCache.php +└── Integration/ ← integrace s ext. službami + ├── Slack/ + └── Stripe/ +\-- + +U menších projektů pochopitelně stačí ploché členění: + +/--pre +Core/ +├── RouterFactory.php +├── Authenticator.php +└── QueueMailer.php +\-- + +Jde o kód, který: + +- Řeší technickou infrastrukturu (routování, logování, cachování) +- Integruje externí služby (Sentry, Elasticsearch, Redis) +- Poskytuje základní služby pro celou aplikaci (mail, databáze) +- Je většinou nezávislý na konkrétní doméně - cache nebo logger funguje stejně pro eshop či blog. + +Tápete, jestli určitá třída patří sem, nebo do modelu? Klíčový rozdíl je v tom, že kód v `Core/`: + +- Neví nic o doméně (produkty, objednávky, články) +- Je většinou možné ho přenést do jiného projektu +- Řeší "jak to funguje" (jak poslat mail), nikoliv "co to dělá" (jaký mail poslat) + +Příklad pro lepší pochopení: + +- `App\Core\MailerFactory` - vytváří instance třídy pro odesílání e-mailů, řeší SMTP nastavení +- `App\Model\OrderMailer` - používá `MailerFactory` k odesílání e-mailů o objednávkách, zná jejich šablony a ví, kdy se mají poslat + + +Příkazové skripty +================= + +Aplikace často potřebují vykonávat činnosti mimo běžné HTTP požadavky - ať už jde o zpracování dat v pozadí, údržbu, nebo periodické úlohy. Pro spouštění slouží jednoduché skripty v adresáři `bin/`, samotnou implementační logiku pak umisťujeme do `app/Tasks/` (případně `app/Commands/`). + +Příklad: + +/--pre +app/Tasks/ +├── Maintenance/ ← údržbové skripty +│ ├── CleanupCommand.php ← mazání starých dat +│ └── DbOptimizeCommand.php ← optimalizace databáze +├── Integration/ ← integrace s externími systémy +│ ├── ImportProducts.php ← import z dodavatelského systému +│ └── SyncOrders.php ← synchronizace objednávek +└── Scheduled/ ← pravidelné úlohy + ├── NewsletterCommand.php ← rozesílání newsletterů + └── ReminderCommand.php ← notifikace zákazníkům +\-- + +Co patří do modelu a co do příkazových skriptů? Například logika pro odeslání jednoho e-mailu je součástí modelu, hromadná rozesílka tisíců e-mailů už patří do `Tasks/`. + +Úlohy obvykle [spouštíme z příkazového řádku |https://blog.nette.org/cs/cli-skripty-v-nette-aplikaci] nebo přes cron. Lze je spouštět i přes HTTP požadavek, ale je nutné myslet na bezpečnost. Presenter, který úlohu spustí, je potřeba zabezpečit, například jen pro přihlášené uživatele nebo silným tokenem a přístupem z povolených IP adres. U dlouhých úloh je nutné zvýšit časový limit skriptu a použít `session_write_close()`, aby se nezamykala session. + + +Další možné adresáře +==================== + +Kromě zmíněných základních adresářů můžete podle potřeb projektu přidat další specializované složky. Podívejme se na nejčastější z nich a jejich použití: + +/--pre +app/ +├── Api/ ← logika pro API nezávislá na prezentační vrstvě +├── Database/ ← migrační skripty a seedery pro testovací data +├── Components/ ← sdílené vizuální komponenty napříč celou aplikací +├── Event/ ← užitečné pokud používáte event-driven architekturu +├── Mail/ ← e-mailové šablony a související logika +└── Utils/ ← pomocné třídy +\-- + +Pro sdílené vizuální komponenty používané v presenterech napříč aplikací lze použít složku `app/Components` nebo `app/Controls`: + +/--pre +app/Components/ +├── Form/ ← sdílené formulářové komponenty +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← komponenty pro výpisy dat +│ └── DataGrid.php +└── Navigation/ ← navigační prvky + ├── Breadcrumbs.php + └── Menu.php +\-- + +Sem patří komponenty, které mají komplexnější logiku. Pokud chcete komponenty sdílet mezi více projekty, je vhodné je vyčlenit do samostatného composer balíčku. + +Do adresáře `app/Mail` můžete umístit správu e-mailové komunikace: + +/--pre +app/Mail/ +├── templates/ ← e-mailové šablony +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Mapování presenterů +=================== + +Mapování definuje pravidla pro odvozování názvu třídy z názvu presenteru. Specifikujeme je v [konfiguraci|configuration] pod klíčem `application › mapping`. + +Na této stránce jsme si ukázali, že presentery umísťujeme do složky `app/Presentation` (případně `app/UI`). Tuto konvenci musíme Nette sdělit v konfiguračním souboru. Stačí jeden řádek: + +```neon +application: + mapping: App\Presentation\*\**Presenter +``` + +Jak mapování funguje? Pro lepší pochopení si nejprve představme aplikaci bez modulů. Chceme, aby třídy presenterů spadaly do jmenného prostoru `App\Presentation`, aby se presenter `Home` mapoval na třídu `App\Presentation\HomePresenter`. Což dosáhneme touto konfigurací: + +```neon +application: + mapping: App\Presentation\*Presenter +``` + +Mapování funguje tak, že název presenteru `Home` nahradí hvězdičku v masce `App\Presentation\*Presenter`, čímž získáme výsledný název třídy `App\Presentation\HomePresenter`. Jednoduché! + +Jak ale vidíte v ukázkách v této a dalších kapitolách, třídy presenterů umisťujeme do eponymních podadresářů, například presenter `Home` se mapuje na třídu `App\Presentation\Home\HomePresenter`. Toho dosáhneme zdvojením hvězdičky (vyžaduje Nette Application 3.2): + +```neon +application: + mapping: App\Presentation\**Presenter +``` + +Nyní přistoupíme k mapování presenterů do modulů. Pro každý modul můžeme definovat specifické mapování: + +```neon +application: + mapping: + Front: App\Presentation\Front\**Presenter + Admin: App\Presentation\Admin\**Presenter + Api: App\Api\*Presenter +``` + +Podle této konfigurace se presenter `Front:Home` mapuje na třídu `App\Presentation\Front\Home\HomePresenter`, zatímco presenter `Api:OAuth` na třídu `App\Api\OAuthPresenter`. + +Protože moduly `Front` i `Admin` mají podobný způsob mapování a takových modulů bude nejspíš více, je možné vytvořit obecné pravidlo, které je nahradí. Do masky třídy tak přibude nová hvězdička pro modul: + +```neon +application: + mapping: + *: App\Presentation\*\**Presenter + Api: App\Api\*Presenter +``` + +Funguje to i pro hlouběji zanořené adresářové struktury, jako je například presenter `Admin:User:Edit`, se segment s hvězdičkou opakuje pro každou úroveň a výsledkem je třída `App\Presentation\Admin\User\Edit\EditPresenter`. + +Alternativním zápisem je místo řetězce použít pole skládající se ze tří segmentů. Tento zápis je ekvivaletní s předchozím: + +```neon +application: + mapping: + *: [App\Presentation, *, **Presenter] + Api: [App\Api, '', *Presenter] +``` diff --git a/application/cs/how-it-works.texy b/application/cs/how-it-works.texy index 978e29c388..0c610439be 100644 --- a/application/cs/how-it-works.texy +++ b/application/cs/how-it-works.texy @@ -22,18 +22,18 @@ Adresářová struktura vypadá nějak takto: /--pre web-project/ ├── app/ ← adresář s aplikací -│ ├── Presenters/ ← presentery a šablony -│ │ ├── HomePresenter.php ← třída presenteru Home -│ │ └── templates/ ← adresář se šablonami -│ │ ├── @layout.latte ← šablona layoutu -│ │ └── Home/ ← šablony presenteru Home -│ │ └── default.latte ← šablona akce 'default' -│ ├── Router/ ← konfigurace URL adres +│ ├── Core/ ← základní třídy nutné pro chod +│ │ └── RouterFactory.php ← konfigurace URL adres +│ ├── Presentation/ ← presentery, šablony & spol. +│ │ ├── @layout.latte ← šablona layoutu +│ │ └── Home/ ← adresář presenteru Home +│ │ ├── HomePresenter.php ← třída presenteru Home +│ │ └── default.latte ← šablona akce default │ └── Bootstrap.php ← zaváděcí třída Bootstrap ├── bin/ ← skripty spouštěné z příkazové řádky ├── config/ ← konfigurační soubory │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← logované chyby ├── temp/ ← dočasné soubory, cache, … ├── vendor/ ← knihovny instalované Composerem @@ -45,9 +45,9 @@ Adresářová struktura vypadá nějak takto: └── .htaccess ← zakazuje přístup do všech adresářů krom www \-- -Adresářovou strukturu můžete jakkoliv měnit, složky přejmenovat či přesunout, a poté pouze upravit cesty k `log/` a `temp/` v souboru `Bootstrap.php` a dále cestu k tomuto souboru v `composer.json` v sekci `autoload`. Nic víc, žádná složitá rekonfigurace, žádné změny konstant. Nette totiž disponuje [chytrou autodetekcí|bootstrap#vyvojarsky-vs-produkcni-rezim]. +Adresářovou strukturu můžete jakkoliv měnit, složky přejmenovat či přesunout, je zcela flexibilní. Nette navíc disponuje chytrou autodetekcí a automaticky rozpozná umístění aplikace včetně její URL základny. -U trošku větších aplikací můžeme složky s presentery a šablonami rozčlenit na disku do podadresářů a třídy do jmenných prostorů, kterým říkáme [moduly |modules]. +U trošku větších aplikací můžeme složky s presentery a šablonami [rozčlenit do podadresářů |directory-structure#Presentery a šablony] a třídy do jmenných prostorů, kterým říkáme moduly. Adresář `www/` představuje tzv. veřejný adresář neboli document-root projektu. Můžete jej přejmenovat bez nutnosti cokoliv dalšího nastavovat na straně aplikace. Jen je potřeba [nakonfigurovat hosting |nette:troubleshooting#Jak změnit či ostranit z URL adresář www] tak, aby document-root mířil do tohoto adresáře. @@ -75,7 +75,7 @@ Jeho úkolem je: Jakou že továrnu? Nevyrábíme přece traktory, ale webové stránky! Vydržte, hned se to vysvětlí. -Slovy „inicializace prostředí“ myslíme například to, že se aktivuje [Tracy|tracy:], což je úžasný nástroj pro logování nebo vizualizaci chyb. Na produkčním serveru chyby loguje, na vývojovém rovnou zobrazuje. Tudíž k inicializaci patří i rozhodnutí, zda web běží v produkčním nebo vývojářském režimu. K tomu Nette používá autodetekci: pokud web spouštíte na localhost, běží v režimu vývojářském. Nemusíte tak nic konfigurovat a aplikace je rovnou připravena jak pro vývoj, tak ostré nasazení. Tyhle kroky se provádějí a jsou podrobně rozepsané v kapitole o [třídě Bootstrap|bootstrap]. +Slovy „inicializace prostředí“ myslíme například to, že se aktivuje [Tracy|tracy:], což je úžasný nástroj pro logování nebo vizualizaci chyb. Na produkčním serveru chyby loguje, na vývojovém rovnou zobrazuje. Tudíž k inicializaci patří i rozhodnutí, zda web běží v produkčním nebo vývojářském režimu. K tomu Nette používá [chytrou autodetekci|bootstrap#vyvojarsky-vs-produkcni-rezim]: pokud web spouštíte na localhost, běží v režimu vývojářském. Nemusíte tak nic konfigurovat a aplikace je rovnou připravena jak pro vývoj, tak ostré nasazení. Tyhle kroky se provádějí a jsou podrobně rozepsané v kapitole o [třídě Bootstrap|bootstrap]. Třetím bodem (ano, druhý jsme přeskočili, ale vrátíme se k němu) je spuštění aplikace. Vyřizování HTTP požadavků má v Nette na starosti třída `Nette\Application\Application` (dále `Application`), takže když říkáme spustit aplikaci, myslíme tím konkrétně zavolání metody s příznačným názvem `run()` na objektu této třídy. @@ -91,7 +91,7 @@ Aplikace psané v Nette se člení do spousty tzv. presenterů (v jiných framew Application začne tím, že požádá tzv. router, aby rozhodl, kterému z presenterů předat aktuální požadavek k vyřízení. Router rozhodne, čí je to zodpovědnost. Podívá se na vstupní URL `https://example.com/product/123` a na základě toho, jak je nastavený, rozhodne, že tohle je práce např. pro **presenter** `Product`, po kterém bude chtít jako **akci** zobrazení (`show`) produktu s `id: 123`. Dvojici presenter + akce je dobrým zvykem zapisovat oddělené dvojtečkou jako `Product:show`. -Tedy router transformoval URL na dvojici `Presenter:action` + parametry, v našem případě `Product:show` + `id: 123`. Jak takový router vypadá se můžete podívat v souboru `app/Router/RouterFactory.php` a podrobně ho popisujeme v kapitole [Routing]. +Tedy router transformoval URL na dvojici `Presenter:action` + parametry, v našem případě `Product:show` + `id: 123`. Jak takový router vypadá se můžete podívat v souboru `app/Core/RouterFactory.php` a podrobně ho popisujeme v kapitole [Routing]. Pojďme dál. Application už zná jméno presenteru a může pokračovat dál. Tím že vyrobí objekt třídy `ProductPresenter`, což je kód presenteru `Product`. Přesněji řečeno, požádá DI kontejner, aby presenter vyrobil, protože od vyrábění je tu on. @@ -121,12 +121,9 @@ Takže, zavolala se metoda `renderShow(123)`, jejíž kód je sice smyšlený p Následně presenter vrátí odpověď. Tou může být HTML stránka, obrázek, XML dokument, odeslání souboru z disku, JSON nebo třeba přesměrování na jinou stránku. Důležité je, že pokud explicitně neřekneme, jak má odpovědět (což je případ `ProductPresenter`), bude odpovědí vykreslení šablony s HTML stránkou. Proč? Protože v 99 % případů chceme vykreslit šablonu, tudíž presenter tohle chování bere jako výchozí a chce nám ulehčit práci. To je smyslem Nette. -Nemusíme ani uvádět, jakou šablonu vykreslit, cestu k ní si odvodí podle jednoduché logiky. V případě presenteru `Product` a akce `show` zkusí, zda existuje jeden z těchto souborů se šablonou uložených relativně od adresáře s třídou `ProductPresenter`: +Nemusíme ani uvádět, jakou šablonu vykreslit, cestu k ní si odvodí sám. V případě akce `show` jednodušše zkusí načíst šablonu `show.latte` v adresáři s třídou `ProductPresenter`. Taktéž se pokusí dohledat layout v souboru `@layout.latte` (podrobněji o [dohledávání šablon|templates#hledani-sablon]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -Taktéž se pokusí dohledat layout v souboru `@layout.latte` a následně šablonu vykreslí. Tím je úkol presenteru i celé aplikace dokonán a dílo jest završeno. Pokud by šablona neexistovala, vrátí se stránka s chybou 404. Více se o presenterech dočtete na stránce [Presentery|presenters]. +A následně šablony vykreslí. Tím je úkol presenteru i celé aplikace dokonán a dílo jest završeno. Pokud by šablona neexistovala, vrátí se stránka s chybou 404. Více se o presenterech dočtete na stránce [Presentery|presenters]. [* request-flow.svg *] @@ -137,7 +134,7 @@ Pro jistotu, zkusme si zrekapitulovat celý proces s trošku jinou URL: 3) router URL dekóduje jako dvojici `Home:default` 4) vytvoří se objekt třídy `HomePresenter` 5) zavolá se metoda `renderDefault()` (pokud existuje) -6) vykreslí se šablona např. `templates/Home/default.latte` s layoutem např. `templates/@layout.latte` +6) vykreslí se šablona např. `default.latte` s layoutem např. `@layout.latte` Možná jste se teď setkali s velkou spoustou nových pojmů, ale věříme, že dávají smysl. Tvorba aplikací v Nette je ohromná pohodička. diff --git a/application/cs/modules.texy b/application/cs/modules.texy deleted file mode 100644 index 2cb3e42719..0000000000 --- a/application/cs/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Moduly -****** - -.[perex] -Moduly představují v Nette logické celky, ze kterých se aplikace skládá. Jejich součástí jsou presentery, šablony, případně i komponenty a modelové třídy. - -S jednou složkou pro presentery a jednou pro šablony bychom si u reálných projektů nevystačili. Mít v jedné složce desítky souborů je minimálně nepřehledné. Jak z toho ven? Jednoduše je na disku rozdělíme do podadresářů a v kódu do jmenných prostorů. A přesně to jsou v Nette moduly. - -Zapomeňme tedy na jednu složku pro presentery a šablony a místo toho vytvoříme moduly, například `Admin` a `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← adresář s moduly -│ ├── Admin/ ← modul Admin -│ │ ├── Presenters/ ← jeho presentery -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← modul Front -│ └── Presenters/ ← jeho presentery -│ └── ... -\-- - -Tuto adresářovou strukturu budou reflektovat jmenné prostory tříd, takže třeba `DashboardPresenter` bude v prostoru `App\Modules\Admin\Presenters`: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Na presenter `Dashboard` uvnitř modulu `Admin` se v rámci aplikace odkazujeme pomocí dvojtečkové notace jako na `Admin:Dashboard`, na jeho akci `default` potom jako na `Admin:Dashboard:default`. -A jak Nette vlastní ví, že `Admin:Dashboard` představuje třídu `App\Modules\Admin\Presenters\DashboardPresenter`? To mu řekneme pomocí [#mapování] v konfiguraci. -Tedy uvedená struktura není pevná a můžete si ji upravit podle potřeb. - -Moduly mohou kromě presenterů a šablon samozřejmě obsahovat všechny další součásti, jako jsou třeba komponenty, modelové třídy, atd. - - -Vnořené moduly --------------- - -Moduly nemusí tvořit jen plochou strukturu, lze vytvářet i submoduly, například: - -/--pre -app/ -├── Modules/ ← adresář s moduly -│ ├── Blog/ ← modul Blog -│ │ ├── Admin/ ← submodul Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← submodul Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← modul Forum -│ │ └── ... -\-- - -Tedy modul `Blog` je rozdělen do submodulů `Admin` a `Front`. A opět se to odrazí na jmenných prostorech, které budou `App\Modules\Blog\Admin\Presenters` apod. Na presenter `Dashboard` uvnitř submodulu se odkazujeme jako `Blog:Admin:Dashboard`. - -Zanořování může pokračovat libovolně hluboko, lze tedy vytvářet sub-submoduly. - - -Vytváření odkazů ----------------- - -Odkazy v šablonách presenterů jsou relativní vůči aktuálnímu modulu. Tedy odkaz `Foo:default` vede na presenter `Foo` v tomtéž modulu, v jakém je aktuální presenter. Pokud je aktuální modul například `Front`, pak odkaz vede takto: - -```latte -odkaz na Front:Product:show -``` - -Odkaz je relativní i pokud je jeho součástí název modulu, ten se pak považuje za submodul: - -```latte -odkaz na Front:Shop:Product:show -``` - -Absolutní odkazy zapisujeme analogicky k absolutním cestám na disku, jen místo lomítek jsou dvojtečky. Tedy absolutní odkaz začíná dvojtečkou: - -```latte -odkaz na Admin:Product:show -``` - -Pro zjištění, zda jsme v určitém modulu nebo jeho submodulu, použijeme funkci `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Routování ---------- - -Viz [kapitola o routování |routing#Moduly]. - - -Mapování --------- - -Definuje pravidla, podle kterých se z názvu presenteru odvodí název třídy. Zapisujeme je v [konfiguraci|configuration] pod klíčem `application › mapping`. - -Začněme ukázkou, která moduly nepoužívá. Budeme jen chtít, aby třídy presenterů měly jmenný prostor `App\Presenters`. Tedy aby se presenter například `Home` mapoval na třídu `App\Presenters\HomePresenter`. Toho lze docílit následující konfigurací: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -Název presenteru se nahradí za hvezdičku v masce třídy a výsledkem je název třídy. Snadné! - -Pokud presentery členíme do modulů, můžeme pro každý modul mít vlastní mapování: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Nyní se presenter `Front:Home` mapuje na třídu `App\Modules\Front\Presenters\HomePresenter` a presenter `Admin:Dashboard` na třídu `App\Modules\Admin\Presenters\DashboardPresenter`. - -Praktičtější bude vytvořit obecné (hvězdičkové) pravidlo, které první dvě nahradí. V masce třídy přibude hvezdička navíc právě pro modul: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Ale co když používáme vícenásobně zanořené moduly a máme třeba presenter `Admin:User:Edit`? V takovém případě se segment s hvězdičkou představující modul pro každou úroveň jednoduše zopakuje a výsledkem bude třída `App\Modules\Admin\User\Presenters\EditPresenter`. - -Alternativním zápisem je místo řetězce použít pole skládající se ze tří segmentů. Tento zápis je ekvivaletní s předchozím: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -Výchozí hodnotou je `*: *Module\*Presenter`. diff --git a/application/cs/presenters.texy b/application/cs/presenters.texy index 41be48834b..06ff5d50b1 100644 --- a/application/cs/presenters.texy +++ b/application/cs/presenters.texy @@ -60,7 +60,7 @@ Obdoba metody `render()`. Zatímco `render()` je určená k tomu, ab Důležité je, že `action()` se volá dříve než `render()`, takže v ní můžeme případně změnit další běh dějin, tj. změnit šablonu, která se bude kreslit, a také metodu `render()`, která se bude volat. A to pomocí `setView('jineView')`. -Metodě se předávají parametry z požadavku. Je možné a doporučené uvést parametrům typy, např. `actionShow(int $id, string $slug = null)` - pokud bude parametr `id` chybět nebo pokud nebude integer, presenter vrátí [chybu 404|#Chyba 404 a spol.] a ukončí činnost. +Metodě se předávají parametry z požadavku. Je možné a doporučené uvést parametrům typy, např. `actionShow(int $id, ?string $slug = null)` - pokud bude parametr `id` chybět nebo pokud nebude integer, presenter vrátí [chybu 404|#Chyba 404 a spol.] a ukončí činnost. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ $this->redirect(/* ... */); // a přesměrujeme Chyba 404 a spol. ================= -Pokud nelze splnit požadavek, třeba z důvodu, že článek který chceme zobrazit neexistuje v databázi, vyhodíme chybu 404 metodou `error(string $message = null, int $httpCode = 404)`. +Pokud nelze splnit požadavek, třeba z důvodu, že článek který chceme zobrazit neexistuje v databázi, vyhodíme chybu 404 metodou `error(?string $message = null, int $httpCode = 404)`. ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Parametry požadavku .{data-version:3.1.14} +========================================== + +Presenter a také každá komponenta získává z HTTP požadavku své parametry. Jejich hodnotu zjistíte metodou `getParameter($name)` nebo `getParameters()`. Hodnoty jsou řetězce či pole řetězců, jde v podstatě o surové data získané přímo z URL. + +Pro větší pohodlí doporučujeme parametry zpřístupnit přes property. Stačí je označit atributem `#[Parameter]`: + +```php +use Nette\Application\Attributes\Parameter; // tento řádek je důležitý + +class HomePresenter extends Nette\Application\UI\Presenter +{ + #[Parameter] + public string $theme; // musí být public +} +``` + +U property doporučujeme uvádět i datový typ (např. `string`) a Nette podle něj hodnotu automaticky přetypuje. Hodnoty parametrů lze také [validovat |#Validace parametrů]. + +Při vytváření odkazu lze parametrům hodnotu přímo nastavit: + +```latte +click +``` + + Persistentní parametry ====================== @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Pokud bude `$this->lang` mít hodnotu například `'en'`, tak i odkazy vytvořené pomocí `link()` nebo `n:href` budou obsahovat parameter `lang=en`. A po kliknutí na odkaz bude opět `$this->lang = 'en'`. -U property doporučujeme uvádět i datový typ (např. `string`) a můžete uvést i výchozí hodnotu. Hodnoty parametrů lze [validovat |#Validace persistentních parametrů]. +U property doporučujeme uvádět i datový typ (např. `string`) a můžete uvést i výchozí hodnotu. Hodnoty parametrů lze [validovat |#Validace parametrů]. Persistentní parametry se standardně přenášejí mezi všemi akcemi daného presenteru. Aby se přenášely i mezi více presentery, je potřeba je definovat buď: @@ -307,18 +333,12 @@ Jdeme do hloubky S tím, co jsme si dosud v této kapitole ukázali, si nejspíš úplně vystačíte. Následující řádky jsou určeny těm, kdo se zajímají o presentery do hloubky a chtějí vědět úplně všechno. -Požadavek a parametry ---------------------- +Validace parametrů +------------------ -Požadavek, který vyřizuje presenter, je objekt [api:Nette\Application\Request] a vrací ho metoda presenteru `getRequest()`. Jeho součástí je pole parametrů a každý z nich patří buď některé z komponent, nebo přímo presenteru (což je vlastně také komponenta, byť speciální). Nette tedy parametry přerozdělí a předá mezi jednotlivé komponenty (a presenter) zavoláním metody `loadState(array $params)`. Získat parametry lze metodu `getParameters(): array`, jednotlivě pomocí `getParameter($name)`. Hodnoty parametrů jsou řetězce či pole řetězců, jde v podstatě o surové data získané přímo z URL. +Hodnoty [parametrů požadavku |#Parametry požadavku] a [persistentních parametrů |#Persistentní parametry] přijatých z URL zapisuje do properties metoda `loadState()`. Ta také kontroluje, zda odpovídá datový typ uvedený u property, jinak odpoví chybou 404 a stránka se nezobrazí. - -Validace persistentních parametrů ---------------------------------- - -Hodnoty [persistentních parametrů |#Persistentní parametry] přijatých z URL zapisuje do properties metoda `loadState()`. Ta také kontroluje, zda odpovídá datový typ uvedený u property, jinak odpoví chybou 404 a stránka se nezobrazí. - -Nikdy slepě nevěřte persistentním parametrům, protože mohou být snadno uživatelem přepsány v URL. Takto například ověříme, zda je jazyk `$this->lang` mezi podporovanými. Vhodnou cestou je přepsat zmíněnou metodu `loadState()`: +Nikdy slepě nevěřte parametrům, protože mohou být snadno uživatelem přepsány v URL. Takto například ověříme, zda je jazyk `$this->lang` mezi podporovanými. Vhodnou cestou je přepsat zmíněnou metodu `loadState()`: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,6 +361,8 @@ class ProductPresenter extends Nette\Application\UI\Presenter Uložení a obnovení požadavku ---------------------------- +Požadavek, který vyřizuje presenter, je objekt [api:Nette\Application\Request] a vrací ho metoda presenteru `getRequest()`. + Aktuální požadavek lze uložit do session nebo naopak z ní obnovit a nechat jej presenter znovu vykonat. To se hodí například v situaci, když uživatel vyplňuje formulář a vyprší mu přihlášení. Aby o data nepřišel, před přesměrováním na přihlašovací stránku aktuální požadavek uložíme do session pomocí `$reqId = $this->storeRequest()`, které vrátí jeho identifikátor v podobě krátkého řetězce a ten předáme jako parameter přihlašovacímu presenteru. Po přihlášení zavoláme metodu `$this->restoreRequest($reqId)`, která požadavek vyzvedne ze session a forwarduje na něj. Metoda přitom ověří, že požadavek vytvořil stejný uživatel, jako se nyní přihlásil. Pokud by se přihlásil jiný uživatel nebo klíč byl neplatný, neudělá nic a program pokračuje dál. @@ -362,7 +384,7 @@ K přesměrování nedojde při AJAXovém nebo POST požadavku, protože by doš Kanonizaci můžete vyvolat i manuálně pomocí metody `canonicalize()`, které se podobně jako metodě `link()` předá presenter, akce a parametry. Vyrobí odkaz a porovná ho s aktuální URL adresou. Pokud se liší, tak přesměruje na vygenerovaný odkaz. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // přesměruje, pokud $slug se liší od $realSlug @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Omezení přístupu pomocí `#[Requires]` .{data-version:3.2.2} +----------------------------------------------------------- + +Atribut `#[Requires]` poskytuje pokročilé možnosti pro omezení přístupu k presenterům a jejich metodám. Lze jej použít pro specifikaci HTTP metod, vyžadování AJAXového požadavku, omezení na stejný původ (same origin), a přístup pouze přes forwardování. Atribut lze aplikovat jak na třídy presenterů, tak na jednotlivé metody `action()`, `render()`, `handle()` a `createComponent()`. + +Můžete určit tyto omezení: +- na HTTP metody: `#[Requires(methods: ['GET', 'POST'])]` +- vyžadování AJAXového požadavku: `#[Requires(ajax: true)]` +- přístup pouze ze stejného původu: `#[Requires(sameOrigin: true)]` +- přístup pouze přes forward: `#[Requires(forward: true)]` +- omezení na konkrétní akce: `#[Requires(actions: 'default')]` + +Podrobnosti najdete v návodu [Jak používat atribut Requires |best-practices:attribute-requires]. + + +Kontrola HTTP metody +-------------------- + +Presentery v Nette automaticky ověřují HTTP metodu každého příchozího požadavku. Důvodem pro tuto kontrolu je především bezpečnost. Standardně jsou povoleny metody `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. + +Chcete-li povolit navíc například metodu `OPTIONS`, použijte k tomu atribut `#[Requires]` (od Nette Application v3.2): + +```php +#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] +class MyPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Ve verzi 3.1 se ověření provádí v `checkHttpMethod()`, která zjišťuje, zda je metoda specifikovaná v požadavku obsažena v poli `$presenter->allowedMethods`. Přidání metody udělejte takto: + +```php +class MyPresenter extends Nette\Application\UI\Presenter +{ + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } +} +``` + +Je důležité zdůraznit, že pokud povolíte metodu `OPTIONS`, musíte ji následně také patřičně obsloužit v rámci svého presenteru. Metoda je často používána jako tzv. preflight request, který prohlížeč automaticky odesílá před skutečným požadavkem, když je potřeba zjistit, zda je požadavek povolený z hlediska CORS (Cross-Origin Resource Sharing) politiky. Pokud metodu povolíte, ale neimplementujete správnou odpověď, může to vést k nekonzistencím a potenciálním bezpečnostním problémům. + + Další četba =========== diff --git a/application/cs/routing.texy b/application/cs/routing.texy index 3f58166681..d997388852 100644 --- a/application/cs/routing.texy +++ b/application/cs/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Nebo můžeme použít tuto formu, všimněte si přepisu validačního regulárního výrazu: +Pro detailnější specifikaci lze použít ještě rozšířenější formu, kde kromě výchozích hodnot můžeme nastavit i další vlastnosti parametrů, jako třeba validační regulární výraz (viz parametr `id`): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Tyto upovídanější formáty se hodí pro doplnění dalších metadat. +Je důležité poznamenat, že pokud parametry definované v poli nejsou uvedeny v masce cesty, jejich hodnoty nelze změnit, ani pomocí query parametrů uvedených za otazníkem v URL. Filtry a překlady @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Moduly ------ -Pokud máme více rout, které spadají do společného [modulu |modules], využijeme `withModule()`: +Pokud máme více rout, které spadají do společného [modulu |directory-structure#Presentery a šablony], využijeme `withModule()`: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Začlenění do aplikace ===================== -Abychom vytvořený router zapojili do aplikace, musíme o něm říci DI kontejneru. Nejsnazší cesta je připravit továrnu, která objekt routeru vyrobí, a sdělit v konfiguraci kontejneru, že ji má použít. Dejme tomu, že k tomu účelu napíšeme metodu `App\Router\RouterFactory::createRouter()`: +Abychom vytvořený router zapojili do aplikace, musíme o něm říci DI kontejneru. Nejsnazší cesta je připravit továrnu, která objekt routeru vyrobí, a sdělit v konfiguraci kontejneru, že ji má použít. Dejme tomu, že k tomu účelu napíšeme metodu `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ Do [konfigurace |dependency-injection:services] pak zapíšeme: ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Jakékoliv závislosti, třeba na databázi atd, se předají tovární metodě jako její parametry pomocí [autowiringu|dependency-injection:autowiring]: @@ -663,7 +663,7 @@ Samostatným použitím myslíme využití schopností routeru v aplikaci, kter Takže opět si vytvoříme metodu, která nám sestaví router, např.: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Anebo objekty přímo vyrobíme: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/cs/templates.texy b/application/cs/templates.texy index 5d0de8dc1a..6f69c2e15c 100644 --- a/application/cs/templates.texy +++ b/application/cs/templates.texy @@ -37,29 +37,75 @@ Ta definuje blok `content`, který se vloží na místo `{include content}` v la Hledání šablon -------------- -Cestu k šablonám odvodí presenter podle jednoduché logiky. Zkusí, zda existuje jeden z těchto souborů umístěných relativně od adresáře s třídou presenteru, kde `` je název aktuálního presenteru a `` je název aktuální akce: +Nemusíte v presenterech uvádět, jaká šablona se má vykreslit, framework cestu odvodí sám a ušetří vám psaní. -- `templates//.latte` -- `templates/..latte` +Pokud používáte adresářovou strukturu, kde každý presenter má vlastní adresář, jednodušše umístěte šablonu do tohoto adresáře pod jménem akce (resp. view), tj. pro akci `default` použijte šablonu `default.latte`: -Pokud šablonu nenajde, zkusí hledat ještě v adresáři `templates` o úroveň výš, tj. na stejné úrovni, jako je adresář s třídou presenteru. +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -Pokud ani tam šablonu nenajde, je odpovědí [chyba 404|presenters#Chyba 404 a spol.]. +Pokud používáte strukturu, kde jsou společně presentery v jednom adresáři a šablony ve složce `templates`, uložte ji buď do souboru `..latte` nebo `/.latte`: -Můžete také změnit view pomocí `$this->setView('jineView')`. Nebo místo dohledávání přímo určit jméno souboru se šablonou pomocí `$this->template->setFile('/path/to/template.latte')`. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1. varianta + └── Home/ + └── default.latte ← 2. varianta +\-- + +Adresář `templates` může být umístěn také o úroveň výš, tj. na stejné úrovni, jako je adresář s třídami presenterů. + +Pokud se šablona nenajde, presenter odpoví [chybou 404 - page not found|presenters#Chyba 404 a spol]. + +View změníte pomocí `$this->setView('jineView')`. Také lze přímo určit soubor se šablonou pomocí `$this->template->setFile('/path/to/template.latte')`. .[note] Soubory, kde se dohledávají šablony, lze změnit překrytím metody [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], která vrací pole možných názvů souborů. -Layout se očekává v těchto souborech: -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` layout společný pro více presenterů +Hledání šablony layoutu +----------------------- + +Nette také automaticky dohledává soubor s layoutem. + +Pokud používáte adresářovou strukturu, kde každý presenter má vlastní adresář, umístěte layout buď do složky s presenterem, pokud je specifický jen pro něj, nebo o úroveň výš, pokud je společný pro více presenterů: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← společný layout + └── Home/ + ├── @layout.latte ← jen pro presenter Home + ├── HomePresenter.php + └── default.latte +\-- + +Pokud používáte strukturu, kde jsou společně presentery v jednom adresáři a šablony ve složce `templates`, bude se layout očekávat na těchto místech: + +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← společný layout + ├── Home.@layout.latte ← jen pro Home, 1. varianta + └── Home/ + └── @layout.latte ← jen pro Home, 2. varianta +\-- + +Pokud se presenter nachází v modulu, bude se dohledávat i o další adresářové úrovně výš, podle zanoření modulu. -Kde `` je název aktuálního presenteru a `` je název layoutu, což je standardně `'layout'`. Název lze změnit pomocí `$this->setLayout('jinyLayout')`, takže se budou zkoušet soubory `@jinyLayout.latte`. +Název layoutu lze změnit pomocí `$this->setLayout('layoutAdmin')` a pak se bude očekávat v souboru `@layoutAdmin.latte`. Také lze přímo určit soubor se šablonou layoutu pomocí `$this->setLayout('/path/to/template.latte')`. -Můžete také přímo určit jméno souboru se šablonou layoutu pomocí `$this->setLayout('/path/to/template.latte')`. Pomocí `$this->setLayout(false)` se dohledávání layoutu vypne. +Pomocí `$this->setLayout(false)` nebo značky `{layout none}` uvnitř šablony se dohledávání layoutu vypne. .[note] Soubory, kde se dohledávají šablony layoutu, lze změnit překrytím metody [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], která vrací pole možných názvů souborů. @@ -104,7 +150,7 @@ Anotace `@property-read` je určená pro IDE a statickou analýzu, díky ní bud 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: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void Latte ve verzi 3 nabízí pokročilejší způsob a to vytvoření si [extension |latte:creating-extension] pro každý webový projekt. Kusý příklad takové třídy: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ Zaregistrujeme ji pomocí [konfigurace |configuration#Šablony Latte]: ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ Translator je alternativně možné nastavit pomocí [konfigurace |configuration ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` Poté lze překladač používat například jako filtr `|translate`, a to včetně doplňujících parametrů, které se předají metodě `translate()` (viz `foo, bar`): diff --git a/application/de/@home.texy b/application/de/@home.texy index 91906d2888..588519ad40 100644 --- a/application/de/@home.texy +++ b/application/de/@home.texy @@ -1,36 +1,85 @@ -Nette Application -***************** +Nette Bewerbung +*************** .[perex] -Das Paket `nette/application` ist die Grundlage für die Erstellung interaktiver Webanwendungen. +Nette Application ist der Kern des Nette-Frameworks, das leistungsstarke Werkzeuge für die Erstellung moderner Webanwendungen bietet. Es bietet zahlreiche außergewöhnliche Funktionen, die die Entwicklung erheblich vereinfachen und die Codesicherheit und Wartbarkeit verbessern. -- [Wie funktionieren die Anwendungen? |how-it-works] -- [Bootstrap] -- [Präsentatoren |Presenters] -- [Schablonen |Templates] -- [Module |Modules] -- [Routing |Routing] -- [URL-Links erstellen |creating-links] -- [Interaktive Komponenten |components] -- [AJAX & Schnipsel |ajax] -- [Multiplikator |multiplier] -- [Konfiguration |Configuration] +Installation .[#toc-installation] +--------------------------------- -Einrichtung ------------ - -Laden Sie das Paket herunter und installieren Sie es mit [Composer |best-practices:composer]: +Laden Sie die Bibliothek herunter und installieren Sie sie mit [Composer |best-practices:composer]: ```shell composer require nette/application ``` -| Version | kompatibel mit PHP + +Warum Nette Application wählen? .[#toc-why-choose-nette-application] +-------------------------------------------------------------------- + +Nette war schon immer ein Pionier im Bereich der Webtechnologien. + +**Bidirektionaler Router:** Nette verfügt über ein fortschrittliches Routing-System, das in seiner Bidirektionalität einzigartig ist - es übersetzt nicht nur URLs in Anwendungsaktionen, sondern kann auch URLs in umgekehrter Richtung erzeugen. Dies bedeutet: +- Sie können die URL-Struktur der gesamten Anwendung jederzeit ändern, ohne Template-Dateien zu modifizieren +- URLs werden automatisch kanonisiert, was die SEO verbessert +- Das Routing wird an einer Stelle definiert, nicht in Anmerkungen verstreut + +**Komponenten und Signale:** Das integrierte Komponentensystem, inspiriert von Delphi und React.js, ist einzigartig unter den PHP-Frameworks: +- Ermöglicht die Erstellung wiederverwendbarer UI-Elemente +- Unterstützt die hierarchische Komposition von Komponenten +- Bietet eine elegante Handhabung von AJAX-Anfragen mittels Signalen +- Reichhaltige Bibliothek mit vorgefertigten Komponenten auf [Componette](https://componette.org) + +**AJAX und Snippets:** Nette führte 2009 eine revolutionäre Art der Arbeit mit AJAX ein, noch vor Lösungen wie Hotwire für Ruby on Rails oder Symfony UX Turbo: +- Snippets ermöglichen es, nur Teile der Seite zu aktualisieren, ohne JavaScript zu schreiben +- Automatische Integration mit dem Komponentensystem +- Intelligente Invalidierung von Seitenabschnitten +- Minimale Datenübertragung + +**Intuitive [Latte |latte:] Templates:** Das sicherste Templating-System für PHP mit erweiterten Funktionen: +- Automatischer XSS-Schutz mit kontextabhängigem Escaping +- Erweiterbar mit benutzerdefinierten Filtern, Funktionen und Tags +- Template-Vererbung und Snippets für AJAX +- Ausgezeichnete PHP 8.x-Unterstützung mit Typsystem + +**Dependency Injection:** Nette nutzt Dependency Injection vollständig aus: +- Automatische Weitergabe von Abhängigkeiten (Autowiring) +- Konfiguration im klaren NEON-Format +- Unterstützung für Komponentenfabriken + + +Wichtigste Vorteile .[#toc-main-benefits] +----------------------------------------- + +- **Sicherheit**: Automatischer Schutz vor [Schwachstellen |nette:vulnerability-protection] wie XSS, CSRF, etc. +- **Produktivität**: Weniger Schreibarbeit, mehr Funktionen dank intelligentem Design +- **Debugging**: [Tracy-Debugger |tracy:] mit Routing-Panel +- **Leistung**: Intelligentes Caching-System, träges Laden von Komponenten +- **Flexibilität**: Einfache URL-Änderung auch nach Abschluss der Anwendung +- **Komponenten**: Einzigartiges System von wiederverwendbaren UI-Elementen +- **Modern**: Volle Unterstützung für PHP 8.4+ und Typsystem + + +Erste Anfänge .[#toc-getting-started] +------------------------------------- + +1. [Verständnis der Anwendungen |how-it-works] - Verständnis der grundlegenden Architektur +2. [Präsentatoren |presenters] - Arbeiten mit Präsentatoren und Aktionen +3. [Templates |templates] - Erstellen von Templates in Latte +4. [Routing |routing] - URL-Konfiguration +5. [Interaktive Komponenten |components] - Verwendung des Komponentensystems + + +PHP-Kompatibilität .[#toc-php-compatibility] +-------------------------------------------- + +| Version kompatibel mit PHP |-----------|------------------- -| Nette-Anwendung 4.0 | PHP 8.0 - 8.2 -| Nette Anwendung 3.1 | PHP 7.2 - 8.2 -| Nette Anwendung 3.0 | PHP 7.1 - 8.0 +| Nette Anwendung 4.0 | PHP 8.1 - 8.4 +| Nette-Anwendung 3.2 | PHP 8.1 - 8.4 +| Nette Anwendung 3.1 | PHP 7.2 - 8.3 +| Nette-Anwendung 3.0 | PHP 7.1 - 8.0 | Nette-Anwendung 2.4 | PHP 5.6 - 8.0 -Gilt für die neuesten Patch-Versionen. +Gültig für die neuesten Patch-Versionen. \ No newline at end of file diff --git a/application/de/@left-menu.texy b/application/de/@left-menu.texy index e3d59146db..371f5b977b 100644 --- a/application/de/@left-menu.texy +++ b/application/de/@left-menu.texy @@ -4,7 +4,7 @@ Nette Bewerbung - [Bootstrap] - [Präsentatoren |Presenters] - [Schablonen |Templates] -- [Module |Modules] +- [Struktur des Verzeichnisses |directory-structure] - [Routing |Routing] - [URL-Links erstellen |creating-links] - [Interaktive Komponenten |components] diff --git a/application/de/ajax.texy b/application/de/ajax.texy index 63922c5732..fe36f40a83 100644 --- a/application/de/ajax.texy +++ b/application/de/ajax.texy @@ -3,10 +3,10 @@ AJAX & Schnipsel
    -Moderne Webanwendungen laufen heute zur Hälfte auf einem Server und zur Hälfte in einem Browser. AJAX ist ein wichtiger verbindender Faktor. Welche Unterstützung bietet das Nette Framework? -- Senden von Template-Fragmenten (sogenannte *snippets*) +Im Zeitalter moderner Webanwendungen, bei denen sich die Funktionalität oft zwischen Server und Browser erstreckt, ist AJAX ein wesentliches Verbindungselement. Welche Möglichkeiten bietet das Nette Framework in diesem Bereich? +- Senden von Teilen des Templates, sogenannte Snippets - Übergabe von Variablen zwischen PHP und JavaScript -- Debugging von AJAX-Anwendungen +- Werkzeuge zur Fehlersuche bei AJAX-Anfragen
    @@ -14,29 +14,32 @@ Moderne Webanwendungen laufen heute zur Hälfte auf einem Server und zur Hälfte AJAX-Anfrage .[#toc-ajax-request] ================================= -Eine AJAX-Anfrage unterscheidet sich nicht von einer klassischen Anfrage - der Presenter wird mit einer bestimmten Ansicht und Parametern aufgerufen. Es liegt auch am Präsentator, wie er darauf reagiert: Er kann seine eigene Routine verwenden, die ein HTML-Codefragment (HTML-Snippet), ein XML-Dokument, ein JSON-Objekt oder JavaScript-Code zurückgibt. +Eine AJAX-Anfrage unterscheidet sich im Grunde nicht von einer klassischen HTTP-Anfrage. Ein Presenter wird mit bestimmten Parametern aufgerufen. Es liegt am Präsentator, wie er auf die Anfrage antwortet - er kann Daten im JSON-Format zurückgeben, einen Teil des HTML-Codes, ein XML-Dokument usw. senden. -Auf der Serverseite kann eine AJAX-Anfrage mit Hilfe der Servicemethode erkannt werden [, die die HTTP-Anfrage kapselt |http:request] `$httpRequest->isAjax()` (erkennt auf der Grundlage des HTTP-Headers `X-Requested-With`). Innerhalb des Presenters ist eine Abkürzung in Form der Methode `$this->isAjax()` verfügbar. +Auf der Browserseite initiieren wir eine AJAX-Anfrage mit der Funktion `fetch()`: -Es gibt ein vorverarbeitetes Objekt namens `payload`, das für das Senden von Daten in JSON an den Browser bestimmt ist. - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Erfolg'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // Bearbeitung der Antwort +}); ``` -Um die volle Kontrolle über Ihre JSON-Ausgabe zu haben, verwenden Sie die Methode `sendJson` in Ihrem Presenter. Sie beendet den Presenter sofort und Sie können auf eine Vorlage verzichten: +Auf der Serverseite wird eine AJAX-Anfrage durch die Methode `$httpRequest->isAjax()` des Dienstes erkannt [, der die HTTP-Anfrage kapselt |http:request]. Sie verwendet den HTTP-Header `X-Requested-With` und muss daher unbedingt gesendet werden. Innerhalb des Presenters können Sie die Methode `$this->isAjax()` verwenden. + +Wenn Sie Daten im JSON-Format senden möchten, verwenden Sie die [`sendJson()` |presenters#Sending a response] Methode. Die Methode beendet auch die Aktivität des Presenters. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Wenn wir HTML senden wollen, können wir entweder eine spezielle Vorlage für AJAX-Anfragen festlegen: +Wenn Sie vorhaben, mit einer speziellen Vorlage für AJAX zu antworten, können Sie dies wie folgt tun: ```php public function handleClick($param): void @@ -44,27 +47,43 @@ public function handleClick($param): void if ($this->isAjax()) { $this->template->setFile('path/to/ajax.latte'); } - // ... + //... } ``` +Schnipsel .[#toc-snippets] +========================== + +Das mächtigste Werkzeug, das Nette für die Verbindung zwischen Server und Client bietet, sind Snippets. Mit ihnen lässt sich eine gewöhnliche Anwendung mit minimalem Aufwand und ein paar Zeilen Code in eine AJAX-Anwendung verwandeln. Das Fifteen-Beispiel zeigt, wie das Ganze funktioniert, und der Code ist auf [GitHub |https://github.com/nette-examples/fifteen] zu finden. + +Mit Snippets oder Clippings können Sie nur Teile der Seite aktualisieren, anstatt die gesamte Seite neu zu laden. Das ist schneller und effizienter und bietet zudem eine komfortablere Benutzererfahrung. Snippets erinnern Sie vielleicht an Hotwire für Ruby on Rails oder Symfony UX Turbo. Interessanterweise hat Nette Snippets schon 14 Jahre früher eingeführt. + +Wie funktionieren Snippets? Wenn die Seite zum ersten Mal geladen wird (eine Nicht-AJAX-Anfrage), wird die gesamte Seite, einschließlich aller Snippets, geladen. Wenn der Benutzer mit der Seite interagiert (z. B. auf eine Schaltfläche klickt, ein Formular ausfüllt usw.), wird nicht die gesamte Seite geladen, sondern eine AJAX-Anfrage gestellt. Der Code im Presenter führt die Aktion aus und entscheidet, welche Snippets aktualisiert werden müssen. Nette rendert diese Schnipsel und sendet sie in Form eines JSON-Arrays. Der Verarbeitungscode im Browser fügt dann die empfangenen Snippets wieder in die Seite ein. Es wird also nur der Code der geänderten Snippets übertragen, was im Vergleich zur Übertragung des gesamten Seiteninhalts Bandbreite spart und das Laden beschleunigt. + + Naja .[#toc-naja] -================= +----------------- -Die [Naja-Bibliothek |https://naja.js.org] wird verwendet, um AJAX-Anfragen auf der Browserseite zu verarbeiten. [Installieren |https://naja.js.org/#/guide/01-install-setup-naja] Sie sie als node.js-Paket (zur Verwendung mit Webpack, Rollup, Vite, Parcel und mehr): +Um Snippets auf der Browserseite zu verarbeiten, wird die [Naja-Bibliothek |https://naja.js.org] verwendet. [Installieren Sie sie |https://naja.js.org/#/guide/01-install-setup-naja] als node.js-Paket (zur Verwendung mit Anwendungen wie Webpack, Rollup, Vite, Parcel und anderen): ```shell npm install naja ``` -...oder fügen Sie sie direkt in die Seitenvorlage ein: +... oder fügen Sie sie direkt in die Seitenvorlage ein: ```html ``` -Um eine AJAX-Anfrage aus einem regulären Link (Signal) oder einer Formularübermittlung zu erzeugen, markieren Sie einfach den entsprechenden Link, das Formular oder die Schaltfläche mit der Klasse `ajax`: +Zunächst müssen Sie die Bibliothek [initialisieren |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: + +```js +naja.initialize(); +``` + +Um einen gewöhnlichen Link (Signal) oder eine Formularübermittlung zu einer AJAX-Anfrage zu machen, markieren Sie einfach den entsprechenden Link, das Formular oder die Schaltfläche mit der Klasse `ajax`: ```html Go @@ -74,64 +93,39 @@ Um eine AJAX-Anfrage aus einem regulären Link (Signal) oder einer Formularüber or +
    ``` -Schnipsel .[#toc-snippets] -========================== - -Es gibt ein weitaus mächtigeres Werkzeug der eingebauten AJAX-Unterstützung - Snippets. Mit ihnen ist es möglich, eine normale Anwendung mit nur wenigen Zeilen Code in eine AJAX-Anwendung zu verwandeln. Wie das Ganze funktioniert, wird im Fifteen-Beispiel gezeigt, dessen Code auch im Build oder auf [GitHub |https://github.com/nette-examples/fifteen] verfügbar ist. - -Die Funktionsweise der Snippets besteht darin, dass bei der ersten (d.h. Nicht-AJAX-) Anfrage die gesamte Seite übertragen wird und dann bei jeder [AJAX-Unteranfrage |components#signal] (Anfrage derselben Ansicht desselben Präsentators) nur der Code der geänderten Teile in das bereits erwähnte Repository `payload` übertragen wird. - -Snippets erinnern vielleicht an Hotwire für Ruby on Rails oder Symfony UX Turbo, aber Nette hat sie schon vierzehn Jahre früher erfunden. +Schnipsel neu zeichnen .[#toc-redrawing-snippets] +------------------------------------------------- - -Invalidierung von Snippets .[#toc-invalidation-of-snippets] -=========================================================== - -Jeder Nachkomme der Klasse [Control |components] (also auch ein Presenter) ist in der Lage, sich zu merken, ob es während einer Anfrage Änderungen gab, die ein erneutes Rendern erforderlich machen. Dafür gibt es zwei Methoden: `redrawControl()` und `isControlInvalid()`. Ein Beispiel: +Jedes Objekt der Klasse [Control |components] (einschließlich des Presenters selbst) hält fest, ob Änderungen eingetreten sind, die ein erneutes Zeichnen erforderlich machen. Zu diesem Zweck wird die Methode `redrawControl()` verwendet. ```php public function handleLogin(string $user): void { - // Das Objekt muss neu gerendert werden, nachdem sich der Benutzer angemeldet hat + // nach dem Einloggen muss der entsprechende Teil neu gezeichnet werden $this->redrawControl(); - // ... + //... } ``` -Nette bietet jedoch eine noch feinere Auflösung als ganze Komponenten. Die aufgeführten Methoden akzeptieren den Namen eines sogenannten "Snippets" als optionalen Parameter. Ein "Snippet" ist im Grunde ein Element in Ihrer Vorlage, das zu diesem Zweck durch ein Latte-Makro markiert wurde, mehr dazu später. So ist es möglich, eine Komponente aufzufordern, nur *Teile* ihrer Vorlage neu zu zeichnen. Wenn die gesamte Komponente ungültig ist, werden alle ihre Schnipsel neu gerendert. Eine Komponente ist auch dann "ungültig", wenn eine ihrer Unterkomponenten ungültig ist. -```php -$this->isControlInvalid(); // -> false +Nette ermöglicht auch eine genauere Kontrolle darüber, was neu gezeichnet werden muss. Die oben erwähnte Methode kann den Snippet-Namen als Argument verwenden. So ist es möglich, auf der Ebene des Schablonenteils zu invalidieren (d.h. ein Neuzeichnen zu erzwingen). Wird die gesamte Komponente für ungültig erklärt, wird auch jedes Snippet davon neu gezeichnet: -$this->redrawControl('header'); // macht das Snippet namens 'header' ungültig -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, mindestens ein Snippet ist ungültig - -$this->redrawControl(); // macht die gesamte Komponente ungültig, jedes Snippet -$this->isControlInvalid('footer'); // -> true +```php +// macht den "Header"-Schnipsel ungültig +$this->redrawControl('header'); ``` -Eine Komponente, die ein Signal erhält, wird automatisch zum Neuzeichnen markiert. - -Dank des Snippet-Redrawing wissen wir genau, welche Teile welcher Elemente neu gezeichnet werden sollten. - - -Tag `{snippet} … {/snippet}` .{toc: Tag snippet} -================================================ - -Das Rendering der Seite verläuft ganz ähnlich wie bei einer normalen Anfrage: Es werden die gleichen Vorlagen geladen usw. Entscheidend ist jedoch, dass die Teile, die nicht in die Ausgabe gelangen sollen, weggelassen werden; die anderen Teile sollen mit einem Bezeichner verknüpft und in einem für einen JavaScript-Handler verständlichen Format an den Benutzer gesendet werden. - -Syntax .[#toc-syntax] ---------------------- +Schnipsel in Latte .[#toc-snippets-in-latte] +-------------------------------------------- -Wenn ein Steuerelement oder ein Snippet in der Vorlage vorhanden ist, müssen wir es mit dem `{snippet} ... {/snippet}` pair-Tag einpacken - er sorgt dafür, dass das gerenderte Snippet "ausgeschnitten" und an den Browser gesendet wird. Außerdem wird es in ein Hilfs-Tag eingebettet `
    ` Tag (es ist möglich, einen anderen zu verwenden). Im folgenden Beispiel wird ein Snippet mit dem Namen `header` definiert. Es kann ebenso gut die Vorlage einer Komponente darstellen: +Die Verwendung von Snippets in Latte ist extrem einfach. Um einen Teil der Vorlage als Snippet zu definieren, umhüllen Sie ihn einfach mit den Tags `{snippet}` und `{/snippet}`: ```latte {snippet header} @@ -139,7 +133,9 @@ Wenn ein Steuerelement oder ein Snippet in der Vorlage vorhanden ist, müssen wi {/snippet} ``` -Wenn Sie ein Snippet mit einem anderen Element erstellen möchten als `
    ` erstellen oder dem Element benutzerdefinierte Attribute hinzufügen möchten, können Sie die folgende Definition verwenden: +Das Snippet erzeugt ein Element `
    ` in der HTML-Seite mit einem speziell generierten `id`. Wenn ein Snippet neu gezeichnet wird, wird der Inhalt dieses Elements aktualisiert. Daher müssen beim ersten Rendering der Seite alle Snippets ebenfalls gerendert werden, auch wenn sie zunächst leer sind. + +Sie können auch ein Snippet mit einem anderen Element als `
    ` mit einem n:-Attribut erstellen: ```latte
    @@ -148,138 +144,106 @@ Wenn Sie ein Snippet mit einem anderen Element erstellen möchten als `
    ` er ``` -Dynamische Schnipsel .[#toc-dynamic-snippets] -============================================= +Schnipsel Bereiche .[#toc-snippet-areas] +---------------------------------------- -In Nette können Sie auch Snippets mit einem dynamischen Namen definieren, der auf einem Laufzeitparameter basiert. Dies eignet sich besonders für verschiedene Listen, bei denen nur eine Zeile geändert werden muss, aber nicht die ganze Liste mit übertragen werden soll. Ein Beispiel hierfür wäre: +Snippet-Namen können auch Ausdrücke sein: ```latte -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Es gibt ein statisches Snippet namens `itemsContainer`, das mehrere dynamische Snippets enthält: `item-0`, `item-1` und so weiter. +Auf diese Weise erhalten wir mehrere Snippets wie `item-0`, `item-1`, usw. Wenn wir einen dynamischen Ausschnitt (z. B. `item-1`) direkt ungültig machen würden, würde nichts neu gezeichnet werden. Der Grund dafür ist, dass Snippets als echte Auszüge funktionieren und nur sie selbst direkt gerendert werden. In der Vorlage gibt es jedoch technisch gesehen kein Snippet namens `item-1`. Es taucht nur auf, wenn der umgebende Code des Snippets ausgeführt wird, in diesem Fall die foreach-Schleife. Daher markieren wir den Teil der Vorlage, der ausgeführt werden muss, mit dem Tag `{snippetArea}`: -Ein dynamisches Snippet kann nicht direkt neu gezeichnet werden (das erneute Zeichnen von `item-1` hat keine Auswirkung), Sie müssen das übergeordnete Snippet (in diesem Beispiel `itemsContainer`) neu zeichnen. Dies bewirkt, dass der Code des übergeordneten Snippets ausgeführt wird, aber nur seine Unter-Snippets an den Browser gesendet werden. Wenn Sie nur einen der Teil-Snippets senden wollen, müssen Sie die Eingabe für das übergeordnete Snippet so ändern, dass die anderen Teil-Snippets nicht erzeugt werden. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -Im obigen Beispiel müssen Sie sicherstellen, dass bei einer AJAX-Anfrage nur ein Element zum Array `$list` hinzugefügt wird, so dass die Schleife `foreach` nur ein dynamisches Snippet ausgibt. +Und wir werden sowohl das einzelne Snippet als auch den gesamten übergreifenden Bereich neu zeichnen: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * This method returns data for the list. - * Usually this would just request the data from a model. - * For the purpose of this example, the data is hard-coded. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +Es ist auch wichtig, dass das Array `$items` nur die Elemente enthält, die neu gezeichnet werden sollen. -Snippets in einer eingebundenen Vorlage .[#toc-snippets-in-an-included-template] -================================================================================ - -Es kann vorkommen, dass sich das Snippet in einer Vorlage befindet, die von einer anderen Vorlage eingebunden wird. In diesem Fall müssen wir den Einbindungscode in der zweiten Vorlage mit dem `snippetArea` -Makro einschließen, dann zeichnen wir sowohl den Snippet-Bereich als auch das eigentliche Snippet neu. - -Das Makro `snippetArea` sorgt dafür, dass der darin enthaltene Code ausgeführt wird, aber nur das eigentliche Snippet in der eingebundenen Vorlage an den Browser gesendet wird. +Beim Einfügen einer anderen Vorlage in die Hauptvorlage unter Verwendung des `{include}` -Tags, das Snippets enthält, muss die eingefügte Vorlage erneut in ein `snippetArea` -Tag eingeschlossen und sowohl das Snippet als auch der Bereich gemeinsam ungültig gemacht werden: ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* enthalten.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -Sie können es auch mit dynamischen Snippets kombinieren. - - -Hinzufügen und Löschen .[#toc-adding-and-deleting] -================================================== -Wenn Sie ein neues Element in die Liste einfügen und `itemsContainer` ungültig machen, liefert die AJAX-Anfrage Snippets, die das neue Element enthalten, aber der Javascript-Handler kann es nicht darstellen. Das liegt daran, dass es kein HTML-Element mit der neu erstellten ID gibt. +Schnipsel in Komponenten .[#toc-snippets-in-components] +------------------------------------------------------- -In diesem Fall besteht die einfachste Möglichkeit darin, die gesamte Liste in ein weiteres Snippet zu verpacken und alles ungültig zu machen: +Sie können Snippets in [Komponenten |components] erstellen, und Nette wird sie automatisch neu zeichnen. Es gibt jedoch eine bestimmte Einschränkung: Um Snippets neu zu zeichnen, wird die Methode `render()` ohne Parameter aufgerufen. Die Übergabe von Parametern in der Vorlage funktioniert also nicht: ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Senden von Benutzerdaten .[#toc-sending-user-data] +-------------------------------------------------- + +Neben den Snippets können Sie beliebige weitere Daten an den Client senden. Schreiben Sie sie einfach in das `payload` Objekt: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + //... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -Das Gleiche gilt für das Löschen eines Eintrags. Es wäre möglich, ein leeres Snippet zu senden, aber in der Regel können Listen paginiert werden, und es wäre kompliziert, das Löschen eines Elements und das Laden eines anderen (das sich auf einer anderen Seite der paginierten Liste befand) zu implementieren. - -Parameter an die Komponente senden .[#toc-sending-parameters-to-component] -========================================================================== +Senden von Parametern .[#toc-sending-parameters] +================================================ Wenn wir Parameter über eine AJAX-Anfrage an die Komponente senden, egal ob es sich um Signalparameter oder dauerhafte Parameter handelt, müssen wir ihren globalen Namen angeben, der auch den Namen der Komponente enthält. Den vollständigen Namen des Parameters gibt die Methode `getParameterId()` zurück. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -Und behandeln Sie die Methode mit den entsprechenden Parametern in der Komponente. +Eine Handle-Methode mit den entsprechenden Parametern in der Komponente: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/de/bootstrap.texy b/application/de/bootstrap.texy index 590e4e76ef..bc84d8e61a 100644 --- a/application/de/bootstrap.texy +++ b/application/de/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + private Configurator $configurator; + private string $rootDir; + + public function __construct() + { + $this->rootDir = dirname(__DIR__); + // Der Konfigurator ist für das Einrichten der Anwendungsumgebung und der Dienste zuständig. + $this->configurator = new Configurator; + // Legen Sie das Verzeichnis für temporäre Dateien fest, die von Nette erzeugt werden (z. B. kompilierte Vorlagen) + $this->configurator->setTempDirectory($this->rootDir . '/temp'); + } + + public function bootWebApplication(): Nette\DI\Container + { + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); + } + + private function initializeEnvironment(): void { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // Nette ist intelligent, und der Entwicklungsmodus wird automatisch aktiviert, + // oder Sie können ihn für eine bestimmte IP-Adresse aktivieren, indem Sie die folgende Zeile auskommentieren: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Aktiviert Tracy: das ultimative "Schweizer Taschenmesser" zur Fehlersuche. + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: Lädt automatisch alle Klassen im angegebenen Verzeichnis + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Konfigurationsdateien laden + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php .[#toc-index-php] =========================== -Im Falle von Webanwendungen ist die Ausgangsdatei `index.php`, die sich im öffentlichen Verzeichnis `www/` befindet. Sie überlässt es der Klasse `Bootstrap`, die Umgebung zu initialisieren und den `$configurator` zurückzugeben, der den DI-Container erstellt. Dann wird der Dienst `Application` aufgerufen, der die Webanwendung ausführt: +Im Falle von Webanwendungen ist die primäre Datei `index.php`, die sich im [öffentlichen Verzeichnis |directory-structure#public-directory-www] `www/` befindet. Dadurch wird die Bootstrap-Klasse die Umgebung initialisieren und einen DI-Container erzeugen. Von diesem wird dann der Dienst `Application` abgerufen, der die Webanwendung startet: ```php -// Initialisieren der Umgebung + Abrufen des Configurator-Objekts -$configurator = App\Bootstrap::boot(); -// Erstellen eines DI-Containers -$container = $configurator->createContainer(); -// DI-Container erstellt ein Nette\Application\Application-Objekt +$bootstrap = new App\Bootstrap; +// Initialisierung der Umgebung + Erstellung eines DI-Containers +$container = $bootstrap->bootWebApplication(); +// DI-Container erstellt ein Nette\Anwendung\Anwendungsobjekt $application = $container->getByType(Nette\Application\Application::class); -// Nette-Anwendung starten +// Starten Sie die Nette-Anwendung und bearbeiten Sie die eingehende Anfrage $application->run(); ``` @@ -59,26 +84,42 @@ Wie Sie sehen, hilft die Klasse [api:Nette\Bootstrap\Configurator], die wir nun Entwicklungs- vs. Produktionsmodus .[#toc-development-vs-production-mode] ========================================================================= -Nette unterscheidet zwischen zwei grundlegenden Modi, in denen eine Anfrage ausgeführt wird: Entwicklungs- und Produktionsmodus. Der Entwicklungsmodus ist auf maximalen Komfort für den Programmierer ausgerichtet, Tracy wird angezeigt, der Cache wird automatisch aktualisiert, wenn Vorlagen oder die DI-Containerkonfiguration geändert werden, usw. Im Produktionsmodus liegt der Schwerpunkt auf der Leistung, Tracy protokolliert nur Fehler und Änderungen an Vorlagen und anderen Dateien werden nicht überprüft. +Nette verhält sich unterschiedlich, je nachdem, ob es auf einem Entwicklungs- oder Produktionsserver ausgeführt wird: + +🛠️ Entwicklungsmodus: + - Zeigt die Tracy-Debug-Leiste mit nützlichen Informationen an (z. B. SQL-Abfragen, Ausführungszeit, Speicherverbrauch). + - Zeigt eine detaillierte Fehlerseite mit Funktionsaufrufspuren und Variableninhalten, wenn ein Fehler auftritt. + - Aktualisiert automatisch den Cache, wenn Latte-Vorlagen, Konfigurationsdateien usw. geändert werden. + + +🚀 Produktionsmodus: + - Es werden keine Debugging-Informationen angezeigt; alle Fehler werden protokolliert. + - Zeigt eine `ErrorPresenter` oder eine allgemeine "Server Error"-Seite an, wenn ein Fehler auftritt. + - Der Cache wird nie automatisch aktualisiert! + - Optimiert für Geschwindigkeit und Sicherheit. -Die Auswahl des Modus erfolgt durch automatische Erkennung, so dass in der Regel keine Notwendigkeit besteht, etwas manuell zu konfigurieren oder umzuschalten. Der Modus ist Entwicklung, wenn die Anwendung auf localhost läuft (d.h. IP-Adresse `127.0.0.1` oder `::1`) und kein Proxy vorhanden ist (d.h. sein HTTP-Header). Ansonsten läuft sie im Produktionsmodus. + +Der Modus wird automatisch bestimmt, so dass es in den meisten Fällen nicht notwendig ist, ihn manuell zu konfigurieren oder zu wechseln: + +- Entwicklungsmodus: Aktiv auf localhost (IP-Adresse `127.0.0.1` oder `::1`), es sei denn, es wird ein Proxy verwendet (d. h. basierend auf den HTTP-Headern). +- Produktionsmodus: Überall sonst aktiv. Wenn Sie den Entwicklungsmodus in anderen Fällen aktivieren möchten, z. B. für Programmierer, die von einer bestimmten IP-Adresse aus zugreifen, können Sie `setDebugMode()` verwenden: ```php -$configurator->setDebugMode('23.75.345.200'); // eine oder mehrere IP-Adressen +$this->configurator->setDebugMode('23.75.345.200'); // eine oder mehrere IP-Adressen ``` Wir empfehlen auf jeden Fall, eine IP-Adresse mit einem Cookie zu kombinieren. Wir speichern ein geheimes Token im `nette-debug` Cookie, z.B. `secret1234`, und der Entwicklungsmodus wird für Programmierer mit dieser Kombination von IP und Cookie aktiviert. ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` Wir können den Entwicklermodus auch komplett abschalten, sogar für localhost: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` Beachten Sie, dass der Wert `true` den Entwicklermodus standardmäßig einschaltet, was auf einem Produktionsserver niemals passieren sollte. @@ -90,7 +131,7 @@ Debugging-Werkzeug Tracy .[#toc-debugging-tool-tracy] Zur einfachen Fehlersuche werden wir das großartige Tool [Tracy |tracy:] einschalten. Im Entwicklermodus zeigt es Fehler an und im Produktionsmodus protokolliert es Fehler in das angegebene Verzeichnis: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ Temporäre Dateien .[#toc-temporary-files] Nette verwendet den Cache für DI-Container, RobotLoader, Vorlagen usw. Daher ist es notwendig, den Pfad zu dem Verzeichnis festzulegen, in dem der Cache gespeichert werden soll: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` Unter Linux oder macOS setzen Sie die [Schreibrechte |nette:troubleshooting#Setting directory permissions] für die Verzeichnisse `log/` und `temp/`. @@ -112,7 +153,7 @@ RobotLoader .[#toc-robotloader] Normalerweise wollen wir die Klassen automatisch mit [RobotLoader |robot-loader:] laden, also müssen wir ihn starten und ihn Klassen aus dem Verzeichnis laden lassen, in dem sich `Bootstrap.php` befindet (d.h. `__DIR__`) und aus allen seinen Unterverzeichnissen: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ Zeitzone .[#toc-timezone] Configurator ermöglicht es Ihnen, eine Zeitzone für Ihre Anwendung festzulegen. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ Im Entwicklungsmodus wird der Container jedes Mal automatisch aktualisiert, wenn Konfigurationsdateien werden mit `addConfig()` geladen: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` Die Methode `addConfig()` kann mehrfach aufgerufen werden, um mehrere Dateien hinzuzufügen. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ Statische Parameter .[#toc-static-parameters] Parameter, die in Konfigurationsdateien verwendet werden, können [im Abschnitt `parameters` |dependency-injection:configuration#parameters] definiert und auch von der Methode `addStaticParameters()` übergeben (oder überschrieben) werden (sie hat den Alias `addParameters()`). Wichtig ist, dass unterschiedliche Parameterwerte die Erzeugung zusätzlicher DI-Container, d.h. zusätzlicher Klassen, bewirken. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ Dynamische Parameter .[#toc-dynamic-parameters] Wir können dem Container auch dynamische Parameter hinzufügen, deren unterschiedliche Werte, im Gegensatz zu statischen Parametern, nicht die Erzeugung neuer DI-Container verursachen. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ Umgebungsvariablen können mit dynamischen Parametern leicht verfügbar gemacht werden. Wir können über `%env.variable%` in Konfigurationsdateien auf sie zugreifen. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ Sie können die folgenden statischen Parameter in den Konfigurationsdateien verw - `%wwwDir%` ist der absolute Pfad zu dem Verzeichnis, das die `index.php` Eintragsdatei enthält - `%tempDir%` ist der absolute Pfad zu dem Verzeichnis für temporäre Dateien - `%vendorDir%` ist der absolute Pfad zu dem Verzeichnis, in dem Composer die Bibliotheken installiert +- `%rootDir%` ist der absolute Pfad zum Stammverzeichnis des Projekts - `%debugMode%` gibt an, ob sich die Anwendung im Debug-Modus befindet - `%consoleMode%` zeigt an, ob die Anfrage über die Befehlszeile kam @@ -225,7 +268,7 @@ services: Erstellen Sie eine neue Instanz und fügen Sie sie in Bootstrap ein: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Verschiedene Umgebungen .[#toc-different-environments] ====================================================== -Es steht Ihnen frei, die Klasse `Bootstrap` an Ihre Bedürfnisse anzupassen. Sie können der Methode `boot()` Parameter hinzufügen, um Webprojekte zu unterscheiden, oder andere Methoden hinzufügen, wie `bootForTests()`, die die Umgebung für Unit-Tests initialisiert, `bootForCli()` für Skripte, die von der Befehlszeile aus aufgerufen werden, und so weiter. +Zögern Sie nicht, die Klasse `Bootstrap` nach Ihren Bedürfnissen anzupassen. Sie können der Methode `bootWebApplication()` Parameter hinzufügen, um zwischen Webprojekten zu unterscheiden. Alternativ können Sie auch andere Methoden hinzufügen, z. B. `bootTestEnvironment()`, um die Umgebung für Unit-Tests zu initialisieren, `bootConsoleApplication()` für Skripte, die von der Befehlszeile aus aufgerufen werden, und so weiter. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container +{ + Tester\Environment::setup(); // Initialisierung des Nette-Testers + $this->setupContainer(); + return $this->configurator->createContainer(); +} + +public function bootConsoleApplication(): Nette\DI\Container { - $configurator = self::boot(); - Tester\Environment::setup(); // Nette Tester Initialisierung - return $configurator; + $this->configurator->setDebugMode(false); + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); } ``` diff --git a/application/de/components.texy b/application/de/components.texy index b4540b4808..d47cc5bd78 100644 --- a/application/de/components.texy +++ b/application/de/components.texy @@ -230,6 +230,28 @@ In der Vorlage stehen diese Meldungen in der Variablen `$flashes` als Objekte `s ``` +Umleitung nach einem Signal .[#toc-redirection-after-a-signal] +============================================================== + +Nach der Verarbeitung eines Komponentensignals folgt oft eine Umleitung. Diese Situation ist ähnlich wie bei Formularen - nach dem Absenden eines Formulars leiten wir ebenfalls um, um eine erneute Übermittlung von Daten zu verhindern, wenn die Seite im Browser aktualisiert wird. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Da eine Komponente ein wiederverwendbares Element ist und in der Regel keine direkte Abhängigkeit von bestimmten Presentern haben sollte, interpretieren die Methoden `redirect()` und `link()` den Parameter automatisch als Komponentensignal: + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +Wenn Sie zu einem anderen Präsentator oder einer Aktion umleiten müssen, können Sie dies über den Präsentator tun: + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Dauerhafte Parameter .[#toc-persistent-parameters] ================================================== @@ -347,7 +369,7 @@ services: Schließlich werden wir diese Fabrik in unserem Präsentator verwenden: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ Komponenten im Detail .[#toc-components-in-depth] Komponenten in einer Nette-Anwendung sind die wiederverwendbaren Teile einer Web-Anwendung, die wir in Seiten einbetten, was das Thema dieses Kapitels ist. Was genau sind die Fähigkeiten einer solchen Komponente? 1) sie ist in einer Vorlage renderbar -2) sie weiß, welcher Teil von ihr bei einer [AJAX-Anfrage |ajax#invalidation] gerendert werden soll (Snippets) +2) es weiß, [welcher Teil von sich selbst |ajax#snippets] während einer AJAX-Anfrage zu rendern ist (Snippets) 3) er kann seinen Zustand in einer URL speichern (persistente Parameter) 4) es hat die Fähigkeit, auf Benutzeraktionen zu reagieren (Signale) 5) er erstellt eine hierarchische Struktur (wobei die Wurzel der Präsentator ist) diff --git a/application/de/configuration.texy b/application/de/configuration.texy index da3a0fcebf..de4d41b0b7 100644 --- a/application/de/configuration.texy +++ b/application/de/configuration.texy @@ -13,11 +13,15 @@ Anwendung: # zeigt das Feld "Nette Anwendung" in Tracy BlueScreen? debugger: ... # (bool) standardmäßig true - # wird der error-presenter im Fehlerfall aufgerufen? - catchExceptions: ... # (bool) steht im Produktionsmodus standardmäßig auf true + # wird error-presenter im Fehlerfall aufgerufen? + # hat nur im Entwicklermodus Auswirkungen + catchExceptions: ... # (bool) ist standardmäßig true # Name des Fehlermelders - errorPresenter: Error # (string) Standardwert ist 'Nette:Error' + errorPresenter: Error # (string|array) Standardwert ist 'Nette:Error' + + # definiert Aliase für Moderatoren und Veranstaltungen + aliases: ... # definiert die Regeln für die Auflösung des Presenter-Namens in eine Klasse mapping: ... @@ -27,11 +31,20 @@ Anwendung: silentLinks: ... # (bool) ist standardmäßig auf false eingestellt ``` -Da Fehlerpräsenter im Entwicklungsmodus standardmäßig nicht aufgerufen werden und die Fehler von Tracy angezeigt werden, hilft das Ändern des Wertes `catchExceptions` in `true` dabei, zu überprüfen, ob die Fehlerpräsenter während der Entwicklung korrekt funktionieren. +Ab `nette/application` Version 3.2 ist es möglich, ein Paar von Fehlerpräsentern zu definieren: + +```neon +application: + errorPresenter: + 4xx: Error4xx # für Nette\Application\BadRequestException + 5xx: Error5xx # für andere Ausnahmen +``` Die Option `silentLinks` legt fest, wie sich Nette im Entwicklermodus verhält, wenn die Link-Generierung fehlschlägt (z. B. weil kein Presenter vorhanden ist usw.). Der Standardwert `false` bedeutet, dass Nette `E_USER_WARNING` auslöst. Die Einstellung `true` unterdrückt diese Fehlermeldung. In einer Produktionsumgebung wird immer `E_USER_WARNING` aufgerufen. Wir können dieses Verhalten auch beeinflussen, indem wir die Presenter-Variable [$invalidLinkMode |creating-links#Invalid Links] setzen. -Das [Mapping definiert die Regeln |modules#mapping], nach denen der Klassenname aus dem Presenter-Namen abgeleitet wird. +[Aliasnamen vereinfachen das Aufsuchen |creating-links#aliases] häufig verwendeter Moderatoren. + +Die [Zuordnung definiert die Regeln |directory-structure#Presenter Mapping], nach denen der Klassenname aus dem Namen des Präsentators abgeleitet wird. Automatische Registrierung von Präsentatoren .[#toc-automatic-registration-of-presenters] @@ -82,6 +95,9 @@ Latte: # aktiviert die [Überprüfung von generiertem Code |latte:develop#Checking Generated Code] phpLinter: ... # (string) Voreinstellung ist null + # legt das Gebietsschema fest + locale: cs_CZ # (string) Voreinstellung ist null + # Klasse von $this->template templateClass: App\MyTemplateClass # Standardwert ist Nette\Bridges\ApplicationLatte\DefaultTemplate ``` @@ -91,7 +107,7 @@ Wenn Sie Latte Version 3 verwenden, können Sie neue [Erweiterungen |latte:creat ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` /--comment diff --git a/application/de/creating-links.texy b/application/de/creating-links.texy index 1ec3fbd1e9..4e4210e9a7 100644 --- a/application/de/creating-links.texy +++ b/application/de/creating-links.texy @@ -38,7 +38,7 @@ Es ist auch möglich, benannte Parameter zu übergeben. Der folgende Link überg detail ``` -Wenn die Methode `ProductPresenter::renderShow()` nicht `$lang` in ihrer Signatur hat, kann sie den Wert des Parameters mit `$lang = $this->getParameter('lang')` lesen. +Wenn die Methode `ProductPresenter::renderShow()` nicht `$lang` in ihrer Signatur hat, kann sie den Wert des Parameters über `$lang = $this->getParameter('lang')` oder über die [Eigenschaft |presenters#Request Parameters] abrufen. Wenn die Parameter in einem Array gespeichert sind, können sie mit dem `...` -Operator (oder `(expand)` -Operator in Latte 2.x) expandiert werden: @@ -103,7 +103,7 @@ Wenn die Aktion `default` lautet, können wir sie weglassen, aber der Doppelpunk home ``` -Links können auch auf andere [Module |modules] verweisen. Hier unterscheidet man zwischen relativen und absoluten Links zu den Untermodulen. Das Prinzip ist analog zu Plattenpfaden, nur dass anstelle von Schrägstrichen Doppelpunkte stehen. Nehmen wir an, dass der eigentliche Präsentator Teil des Moduls `Front` ist, dann schreiben wir: +Links können auch auf andere [Module |directory-structure#Presenters and Templates] verweisen. Dabei werden die Links in relativ zu den Untermodulen oder absolut unterschieden. Das Prinzip ist analog zu Plattenpfaden, nur dass anstelle von Schrägstrichen Doppelpunkte stehen. Nehmen wir an, dass der eigentliche Präsentator Teil des Moduls `Front` ist, dann schreiben wir: ```latte link to Front:Shop:Product:show @@ -140,7 +140,7 @@ Das Ziel `this` wird einen Link zur aktuellen Seite erstellen: refresh ``` -Gleichzeitig werden alle Parameter, die in der Signatur des Befehls `render()` oder `action()` Methode angegeben sind, übertragen. Wenn wir uns also auf den Seiten `Product:show` und `id:123` befinden, wird der Link zu `this` auch diesen Parameter übergeben. +Gleichzeitig werden alle Parameter, die in der Signatur des Befehls `action()` oder `render()` Methode angegebenen Parameter, wenn die `action()` nicht definiert ist, übertragen. Wenn wir uns also auf den Seiten `Product:show` und `id:123` befinden, wird der Link zu `this` auch diesen Parameter übergeben. Natürlich ist es auch möglich, die Parameter direkt anzugeben: @@ -213,7 +213,7 @@ Da [Komponenten |components] separate, wiederverwendbare Einheiten sind, die kei Wenn wir auf Präsentatoren in der Komponentenvorlage verlinken wollen, verwenden wir das Tag `{plink}`: ```latte -home +home ``` oder im Code @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Aliasnamen .[#toc-aliases]{data-version:v3.2.2} +=============================================== + +Manchmal ist es sinnvoll, einem Presenter:Action-Paar einen leicht zu merkenden Alias zuzuweisen. Zum Beispiel könnten Sie die Homepage `Front:Home:default` einfach als `home` oder `Admin:Dashboard:default` als `admin` benennen. + +Aliasnamen werden in der [Konfiguration |configuration] unter dem Schlüssel `application › aliases` definiert: + +```neon +application: + aliases: + home: Front:Home:default + admin: Admin:Dashboard:default + sign: Front:Sign:in +``` + +In Links werden sie mit dem at-Symbol geschrieben, zum Beispiel: + +```latte +administration +``` + +Sie werden in allen Methoden unterstützt, die mit Links arbeiten, wie `redirect()` und ähnliche. + + Ungültige Links .[#toc-invalid-links] ===================================== @@ -257,6 +281,6 @@ Wie erstellt man Links mit der Methode `link()` comfort, aber ohne die Anwesenhe LinkGenerator ist ein Dienst, den Sie über den Konstruktor übergeben können und dann Links mit seiner Methode `link()` erstellen. -Es gibt einen Unterschied zu Presentern. LinkGenerator erstellt alle Links als absolute URLs. Außerdem gibt es keinen "aktuellen Präsentator", so dass es nicht möglich ist, nur den Namen der Aktion `link('default')` oder die relativen Pfade zu den [Modulen |modules] anzugeben. +Im Vergleich zu Präsentatoren gibt es einen Unterschied. LinkGenerator erstellt alle Links direkt als absolute URLs. Außerdem gibt es keinen "eigentlichen Präsentator", so dass Sie nicht einfach den Aktionsnamen `link('default')` als Ziel angeben oder relative Pfade zu Modulen auflisten können. Ungültige Links führen immer zu `Nette\Application\UI\InvalidLinkException`. diff --git a/application/de/directory-structure.texy b/application/de/directory-structure.texy new file mode 100644 index 0000000000..fa8341ab7d --- /dev/null +++ b/application/de/directory-structure.texy @@ -0,0 +1,526 @@ +Verzeichnisstruktur der Anwendung +********************************* + +
    + +Wie entwirft man eine klare und skalierbare Verzeichnisstruktur für Projekte in Nette Framework? Wir zeigen Ihnen bewährte Verfahren, die Ihnen helfen, Ihren Code zu organisieren. Sie werden es lernen: + +- wie Sie Ihre Anwendung **logisch** in Verzeichnisse strukturieren +- wie man die Struktur so gestaltet, dass sie bei wachsendem Projekt **gut skalierbar** ist +- was die **möglichen Alternativen** sind und welche Vor- und Nachteile sie haben + +
    + + +Es ist wichtig zu erwähnen, dass das Nette Framework selbst nicht auf einer bestimmten Struktur besteht. Es ist so konzipiert, dass es sich leicht an alle Bedürfnisse und Vorlieben anpassen lässt. + + +Grundlegende Projektstruktur .[#toc-basic-project-structure] +============================================================ + +Obwohl Nette Framework keine feste Verzeichnisstruktur vorgibt, gibt es eine bewährte Standardanordnung in Form von [Web Project |https://github.com/nette/web-project]: + +/--pre +web-project/ +├── app/ ← Anwendungsverzeichnis +├── assets/ ← SCSS, JS-Dateien, Bilder..., alternativ resources/ +├── bin/ ← Befehlszeilenskripte +├── config/ ← Konfiguration +├── log/ ← protokollierte Fehler +├── temp/ ← temporäre Dateien, Cache +├── tests/ ← Tests +├── vendor/ ← vom Composer installierte Bibliotheken +└── www/ ← öffentliches Verzeichnis (document-root) +\-- + +Sie können diese Struktur frei nach Ihren Bedürfnissen verändern - Ordner umbenennen oder verschieben. Dann müssen Sie nur die relativen Pfade zu den Verzeichnissen in `Bootstrap.php` und eventuell `composer.json` anpassen. Mehr ist nicht nötig, keine komplexe Neukonfiguration, keine ständigen Änderungen. Nette verfügt über eine intelligente automatische Erkennung und erkennt automatisch den Speicherort der Anwendung einschließlich ihrer URL-Basis. + + +Grundsätze der Code-Organisation .[#toc-code-organization-principles] +===================================================================== + +Wenn Sie zum ersten Mal ein neues Projekt erkunden, sollten Sie in der Lage sein, sich schnell zu orientieren. Stellen Sie sich vor, Sie klicken auf das Verzeichnis `app/Model/` und sehen diese Struktur: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +Daraus erfahren Sie nur, dass das Projekt einige Dienste, Repositories und Entitäten verwendet. Sie werden nichts über den eigentlichen Zweck der Anwendung erfahren. + +Schauen wir uns einen anderen Ansatz an - **Organisation nach Domänen**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Dies ist anders - auf den ersten Blick ist klar, dass es sich um eine E-Commerce-Site handelt. Die Verzeichnisnamen selbst verraten, was die Anwendung kann - sie arbeitet mit Zahlungen, Bestellungen und Produkten. + +Der erste Ansatz (Organisation nach Klassentyp) bringt in der Praxis mehrere Probleme mit sich: Logisch zusammenhängender Code ist über verschiedene Ordner verstreut und man muss zwischen ihnen hin- und herspringen. Daher werden wir nach Domänen organisieren. + + +Namespaces .[#toc-namespaces] +----------------------------- + +Es ist üblich, dass die Verzeichnisstruktur den Namensräumen in der Anwendung entspricht. Das bedeutet, dass der physische Speicherort von Dateien ihrem Namensraum entspricht. Zum Beispiel sollte eine Klasse, die sich in `app/Model/Product/ProductRepository.php` befindet, den Namensraum `App\Model\Product` haben. Dieses Prinzip hilft bei der Orientierung im Code und vereinfacht das automatische Laden. + + +Singular vs. Plural in Namen .[#toc-singular-vs-plural-in-names] +---------------------------------------------------------------- + +Beachten Sie, dass wir für die Hauptanwendungsverzeichnisse Singular verwenden: `app`, `config`, `log`, `temp`, `www`. Das Gleiche gilt innerhalb der Anwendung: `Model`, `Core`, `Presentation`. Der Grund dafür ist, dass jeder Begriff ein einheitliches Konzept darstellt. + +In ähnlicher Weise repräsentiert `app/Model/Product` alles über Produkte. Wir nennen ihn nicht `Products`, weil es sich nicht um einen Ordner voller Produkte handelt (der Dateien wie `iphone.php`, `samsung.php` enthalten würde). Es ist ein Namensraum, der Klassen für die Arbeit mit Produkten enthält - `ProductRepository.php`, `ProductService.php`. + +Der Ordner `app/Tasks` ist plural, weil er eine Reihe von eigenständigen ausführbaren Skripten enthält - `CleanupTask.php`, `ImportTask.php`. Jedes dieser Skripte ist eine unabhängige Einheit. + +Aus Gründen der Konsistenz empfehlen wir die Verwendung von: +- Singular für Namensräume, die eine funktionale Einheit darstellen (auch wenn mit mehreren Entitäten gearbeitet wird) +- Plural für Sammlungen von unabhängigen Einheiten +- Bei Unsicherheiten oder wenn Sie nicht darüber nachdenken wollen, wählen Sie Singular + + +Öffentliches Verzeichnis `www/` .[#toc-public-directory-www] +============================================================ + +Dieses Verzeichnis ist das einzige, das vom Web aus zugänglich ist (sogenanntes Document-Root). Sie werden oft den Namen `public/` anstelle von `www/` finden - das ist nur eine Frage der Konvention und hat keinen Einfluss auf die Funktionalität. Das Verzeichnis enthält: +- [Einstiegspunkt |bootstrap#index.php] der Anwendung `index.php` +- `.htaccess` Datei mit mod_rewrite Regeln (für Apache) +- Statische Dateien (CSS, JavaScript, Bilder) +- Hochgeladene Dateien + +Für die Sicherheit der Anwendung ist es wichtig, dass [die Dokumenten-Wurzel |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url] korrekt [konfiguriert ist |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url]. + +.[note] +Legen Sie niemals den Ordner `node_modules/` in dieses Verzeichnis - er enthält Tausende von Dateien, die ausführbar sein können und nicht öffentlich zugänglich sein sollten. + + +Anwendungsverzeichnis `app/` .[#toc-application-directory-app] +============================================================== + +Dies ist das Hauptverzeichnis mit dem Anwendungscode. Grundlegende Struktur: + +/--pre +app/ +├── Core/ ← Infrastrukturfragen +├── Model/ ← Geschäftslogik +├── Presentation/ ← Präsentatoren und Vorlagen +├── Tasks/ ← Befehlsskripte +└── Bootstrap.php ← Anwendungs-Bootstrap-Klasse +\-- + +`Bootstrap.php` ist die [Startklasse der Anwendung |bootstrap], die die Umgebung initialisiert, die Konfiguration lädt und den DI-Container erstellt. + +Schauen wir uns nun die einzelnen Unterverzeichnisse im Detail an. + + +Präsentatoren und Vorlagen .[#toc-presenters-and-templates] +=========================================================== + +Wir haben den Präsentationsteil der Anwendung im Verzeichnis `app/Presentation`. Eine Alternative ist das kurze `app/UI`. Dies ist der Ort für alle Präsentatoren, ihre Vorlagen und alle Hilfsklassen. + +Wir organisieren diese Schicht nach Domänen. In einem komplexen Projekt, das E-Commerce, Blog und API kombiniert, würde die Struktur wie folgt aussehen: + +/--pre +app/Presentation/ +├── Shop/ ← E-Commerce-Frontend +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← Blog +│ ├── Home/ +│ └── Post/ +├── Admin/ ← Verwaltung +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← API-Endpunkte + └── V1/ +\-- + +Für ein einfaches Blog hingegen würden wir diese Struktur verwenden: + +/--pre +app/Presentation/ +├── Front/ ← Website-Frontend +│ ├── Home/ +│ └── Post/ +├── Admin/ ← Verwaltung +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, Sitemaps usw. +\-- + +Ordner wie `Home/` oder `Dashboard/` enthalten Moderatoren und Vorlagen. Ordner wie `Front/`, `Admin/` oder `Api/` werden **Module** genannt. Technisch gesehen sind dies reguläre Verzeichnisse, die der logischen Organisation der Anwendung dienen. + +Jeder Ordner mit einem Presenter enthält einen ähnlich benannten Presenter und dessen Vorlagen. Zum Beispiel enthält der Ordner `Dashboard/`: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← Präsentator +└── default.latte ← Vorlage +\-- + +Diese Verzeichnisstruktur spiegelt sich in den Namespaces der Klassen wider. So befindet sich z. B. `DashboardPresenter` im Namensraum `App\Presentation\Admin\Dashboard` (siehe [Presenter-Zuordnung |#presenter mapping]): + +```php +namespace App\Presentation\Admin\Dashboard; + +class DashboardPresenter extends Nette\Application\UI\Presenter +{ + //... +} +``` + +Wir bezeichnen den Presenter `Dashboard` innerhalb des Moduls `Admin` in der Anwendung mit der Doppelpunktschreibweise als `Admin:Dashboard`. Auf seine `default` Aktion dann als `Admin:Dashboard:default`. Für verschachtelte Module verwenden wir mehr Doppelpunkte, zum Beispiel `Shop:Order:Detail:default`. + + +Flexible Strukturentwicklung .[#toc-flexible-structure-development] +------------------------------------------------------------------- + +Einer der großen Vorteile dieser Struktur ist, dass sie sich elegant an wachsende Projektanforderungen anpassen lässt. Nehmen wir als Beispiel den Teil, der XML-Feeds erzeugt. Am Anfang haben wir ein einfaches Formular: + +/--pre +Export/ +├── ExportPresenter.php ← ein Presenter für alle Exporte +├── sitemap.latte ← Vorlage für Sitemap +└── feed.latte ← Vorlage für RSS-Feed +\-- + +Mit der Zeit kommen weitere Feed-Typen hinzu, für die wir mehr Logik benötigen... Kein Problem! Der Ordner `Export/` wird einfach zu einem Modul: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← Feed für Amazon + └── ebay.latte ← Feed für eBay +\-- + +Diese Umwandlung ist völlig problemlos - es müssen lediglich neue Unterordner erstellt, der Code in diese aufgeteilt und die Links aktualisiert werden (z. B. von `Export:feed` zu `Export:Feed:amazon`). Auf diese Weise können wir die Struktur schrittweise nach Bedarf erweitern, die Verschachtelungsebene ist in keiner Weise begrenzt. + +Wenn Sie zum Beispiel in der Verwaltung viele Präsenter haben, die mit der Auftragsverwaltung zusammenhängen, wie `OrderDetail`, `OrderEdit`, `OrderDispatch` usw., können Sie zur besseren Organisation ein Modul (Ordner) `Order` erstellen, das (Ordner für) Präsenter `Detail`, `Edit`, `Dispatch` und andere enthält. + + +Standort der Vorlage .[#toc-template-location] +---------------------------------------------- + +In den vorangegangenen Beispielen haben wir gesehen, dass sich die Vorlagen direkt in dem Ordner mit dem Präsentator befinden: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← Präsentator +├── DashboardTemplate.php ← optionale Vorlagenklasse +└── default.latte ← Vorlage +\-- + +Dieser Speicherort erweist sich in der Praxis als der praktischste - Sie haben alle zugehörigen Dateien direkt zur Hand. + +Alternativ können Sie die Vorlagen auch in einem Unterordner von `templates/` ablegen. Nette unterstützt beide Varianten. Sie können Vorlagen sogar komplett außerhalb des Ordners `Presentation/` ablegen. Alles über die Ablagemöglichkeiten von Vorlagen finden Sie im Kapitel [Vorlagen-Suche |templates#Template Lookup]. + + +Hilfsklassen und Komponenten .[#toc-helper-classes-and-components] +------------------------------------------------------------------ + +Präsentatoren und Vorlagen werden oft mit anderen Hilfsdateien geliefert. Wir ordnen sie logisch nach ihrem Anwendungsbereich an: + +1. **Direkt mit dem Präsentator**, wenn es sich um spezifische Komponenten für den jeweiligen Präsentator handelt: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← Komponente für die Produktauflistung +└── FilterForm.php ← Formular für die Filterung +\-- + +2. **Für Module** - wir empfehlen die Verwendung des Ordners `Accessory`, der ordentlich am Anfang des Alphabets platziert ist: + +/--pre +Front/ +├── Accessory/ +│ ├── NavbarControl.php ← Komponenten für das Frontend +│ └── TemplateFilters.php +├── Product/ +└── Cart/ +\-- + +3. **Für die gesamte Anwendung** - in `Presentation/Accessory/`: +/--pre +app/Presentation/ +├── Accessory/ +│ ├── LatteExtension.php +│ └── TemplateFilters.php +├── Front/ +└── Admin/ +\-- + +Oder Sie können Hilfsklassen wie `LatteExtension.php` oder `TemplateFilters.php` in den Infrastruktur-Ordner `app/Core/Latte/` legen. Und Komponenten in `app/Components`. Die Wahl hängt von den Konventionen des Teams ab. + + +Modell - Herzstück der Anwendung .[#toc-model-heart-of-the-application] +======================================================================= + +Das Modell enthält die gesamte Geschäftslogik der Anwendung. Für seine Organisation gilt die gleiche Regel - wir strukturieren nach Domänen: + +/--pre +app/Model/ +├── Payment/ ← alles über Zahlungen +│ ├── PaymentFacade.php ← Haupteinstiegspunkt +│ ├── PaymentRepository.php +│ ├── Payment.php ← Entität +├── Order/ ← alles über Bestellungen +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← alles über den Versand +\-- + +Im Modell finden Sie typischerweise diese Arten von Klassen: + +**Fassaden**: stellen den Haupteinstiegspunkt in einen bestimmten Bereich der Anwendung dar. Sie fungieren als Orchestrator, der die Zusammenarbeit zwischen verschiedenen Diensten koordiniert, um vollständige Anwendungsfälle zu implementieren (wie "Bestellung erstellen" oder "Zahlung verarbeiten"). Unter ihrer Orchestrierungsschicht verbirgt die Fassade Implementierungsdetails vor dem Rest der Anwendung und bietet so eine saubere Schnittstelle für die Arbeit mit der jeweiligen Domäne. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // Validierung + // Auftragserstellung + // E-Mail-Versand + // Schreiben in die Statistik + } +} +``` + +**Dienste**: konzentrieren sich auf spezifische Geschäftsvorgänge innerhalb einer Domäne. Im Gegensatz zu Fassaden, die ganze Anwendungsfälle orchestrieren, implementiert ein Dienst spezifische Geschäftslogik (wie Preisberechnungen oder Zahlungsverarbeitung). Dienste sind in der Regel zustandslos und können entweder von Fassaden als Bausteine für komplexere Vorgänge oder direkt von anderen Teilen der Anwendung für einfachere Aufgaben verwendet werden. + +```php +class PricingService +{ + public function calculateTotal(Order $order): Money + { + // Preiskalkulation + } +} +``` + +**Repositories**: übernehmen die gesamte Kommunikation mit dem Datenspeicher, in der Regel einer Datenbank. Ihre Aufgabe ist es, Entitäten zu laden und zu speichern und Methoden für die Suche nach ihnen zu implementieren. Ein Repository schirmt den Rest der Anwendung von den Details der Datenbankimplementierung ab und bietet eine objektorientierte Schnittstelle für die Arbeit mit Daten. + +```php +class OrderRepository +{ + public function find(int $id): ?Order + { + } + + public function findByCustomer(int $customerId): array + { + } +} +``` + +**Entitäten**: Objekte, die die wichtigsten Geschäftskonzepte in der Anwendung darstellen, die ihre Identität haben und sich im Laufe der Zeit ändern. In der Regel handelt es sich um Klassen, die mithilfe von ORM (wie Nette Database Explorer oder Doctrine) auf Datenbanktabellen abgebildet werden. Entitäten können Geschäftsregeln für ihre Daten und Validierungslogik enthalten. + +```php +// Entität, die der Datenbanktabelle Aufträge zugeordnet ist +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, + ]); + } +} +``` + +**Wertobjekte**: unveränderliche Objekte, die Werte ohne eigene Identität darstellen - zum Beispiel einen Geldbetrag oder eine E-Mail-Adresse. Zwei Instanzen eines Wertobjekts mit denselben Werten werden als identisch betrachtet. + + +Infrastruktur-Code .[#toc-infrastructure-code] +============================================== + +Der Ordner `Core/` (oder auch `Infrastructure/`) beherbergt die technische Grundlage der Anwendung. Der Infrastrukturcode umfasst in der Regel: + +/--pre +app/Core/ +├── Router/ ← Routing und URL-Verwaltung +│ └── RouterFactory.php +├── Security/ ← Authentifizierung und Autorisierung +│ ├── Authenticator.php +│ └── Authorizator.php +├── Logging/ ← Protokollierung und Überwachung +│ ├── SentryLogger.php +│ └── FileLogger.php +├── Cache/ ← Caching-Schicht +│ └── FullPageCache.php +└── Integration/ ← Integration mit externen Diensten + ├── Slack/ + └── Stripe/ +\-- + +Für kleinere Projekte ist eine flache Struktur natürlich ausreichend: + +/--pre +Core/ +├── RouterFactory.php +├── Authenticator.php +└── QueueMailer.php +\-- + +Dies ist Code, der: + +- die technische Infrastruktur verwaltet (Routing, Protokollierung, Caching) +- Integration externer Dienste (Sentry, Elasticsearch, Redis) +- grundlegende Dienste für die gesamte Anwendung bereitstellt (Mail, Datenbank) +- weitgehend unabhängig von der jeweiligen Domäne ist - Cache oder Logger funktionieren für E-Commerce oder Blog gleichermaßen. + +Sie fragen sich, ob eine bestimmte Klasse hierher oder in das Modell gehört? Der Hauptunterschied ist, dass der Code in `Core/`: + +- Er weiß nichts über die Domäne (Produkte, Bestellungen, Artikel) +- Kann normalerweise in ein anderes Projekt übertragen werden +- Löst die Frage, "wie es funktioniert" (wie man Mails verschickt), nicht "was es tut" (welche Mails verschickt werden sollen) + +Beispiel zum besseren Verständnis: + +- `App\Core\MailerFactory` - erstellt Instanzen der E-Mail-Versandklasse, verwaltet SMTP-Einstellungen +- `App\Model\OrderMailer` - verwendet `MailerFactory`, um E-Mails über Bestellungen zu versenden, kennt deren Vorlagen und weiß, wann sie versendet werden sollen + + +Befehlsskripte .[#toc-command-scripts] +====================================== + +Anwendungen müssen oft Aufgaben außerhalb der regulären HTTP-Anfragen ausführen - sei es die Datenverarbeitung im Hintergrund, die Wartung oder regelmäßige Aufgaben. Einfache Skripte im Verzeichnis `bin/` werden zur Ausführung verwendet, während die eigentliche Implementierungslogik in `app/Tasks/` (oder `app/Commands/`) untergebracht ist. + +Beispiel: + +/--pre +app/Tasks/ +├── Maintenance/ ← Wartungsskripte +│ ├── CleanupCommand.php ← Löschen von alten Daten +│ └── DbOptimizeCommand.php ← Optimierung der Datenbank +├── Integration/ ← Integration mit externen Systemen +│ ├── ImportProducts.php ← Import aus Lieferantensystemen +│ └── SyncOrders.php ← Bestellsynchronisation +└── Scheduled/ ← Regelmäßige Aufgaben + ├── NewsletterCommand.php ← Versand von Newslettern + └── ReminderCommand.php ← Kundenbenachrichtigungen +\-- + +Was gehört in das Modell und was in die Befehlsskripte? Zum Beispiel ist die Logik für den Versand einer E-Mail Teil des Modells, der Massenversand von Tausenden von E-Mails gehört in `Tasks/`. + +Aufgaben werden in der Regel [über die Befehlszeile |https://blog.nette.org/en/cli-scripts-in-nette-application] oder über Cron [ausgeführt |https://blog.nette.org/en/cli-scripts-in-nette-application]. Sie können auch über eine HTTP-Anfrage ausgeführt werden, aber die Sicherheit muss berücksichtigt werden. Der Präsentator, der die Aufgabe ausführt, muss gesichert werden, z. B. nur für angemeldete Benutzer oder mit einem starken Token und Zugriff von erlaubten IP-Adressen. Bei langen Aufgaben muss das Zeitlimit für das Skript erhöht und `session_write_close()` verwendet werden, um ein Sperren der Sitzung zu vermeiden. + + +Andere mögliche Verzeichnisse .[#toc-other-possible-directories] +================================================================ + +Zusätzlich zu den genannten Basisverzeichnissen können Sie je nach Projektbedarf weitere spezialisierte Verzeichnisse hinzufügen. Schauen wir uns die gängigsten Verzeichnisse und ihre Verwendung an: + +/--pre +app/ +├── Api/ ← API-Logik unabhängig von der Präsentationsschicht +├── Database/ ← Migrationsskripte und Seeder für Testdaten +├── Components/ ← gemeinsame visuelle Komponenten für die gesamte Anwendung +├── Event/ ← nützlich bei Verwendung einer ereignisgesteuerten Architektur +├── Mail/ ← E-Mail-Vorlagen und zugehörige Logik +└── Utils/ ← Hilfsklassen +\-- + +Für gemeinsam genutzte visuelle Komponenten, die in Präsentatoren in der gesamten Anwendung verwendet werden, können Sie den Ordner `app/Components` oder `app/Controls` verwenden: + +/--pre +app/Components/ +├── Form/ ← gemeinsame Formular-Komponenten +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← Komponenten für Datenauflistungen +│ └── DataGrid.php +└── Navigation/ ← Navigationselemente + ├── Breadcrumbs.php + └── Menu.php +\-- + +Dorthin gehören Komponenten mit komplexerer Logik. Wenn Sie Komponenten für mehrere Projekte gemeinsam nutzen möchten, sollten Sie sie in ein eigenständiges Composer-Paket aufteilen. + +Im Verzeichnis `app/Mail` können Sie die Verwaltung der E-Mail-Kommunikation unterbringen: + +/--pre +app/Mail/ +├── templates/ ← E-Mail-Vorlagen +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Präsentator-Mapping .[#toc-presenter-mapping] +============================================= + +Mapping definiert Regeln für die Ableitung von Klassennamen aus Presenter-Namen. Wir geben sie in der [Konfiguration |configuration] unter dem Schlüssel `application › mapping` an. + +Auf dieser Seite haben wir gezeigt, dass wir Presenter im Ordner `app/Presentation` (oder `app/UI`) ablegen. Wir müssen Nette über diese Konvention in der Konfigurationsdatei informieren. Eine Zeile reicht aus: + +```neon +application: + mapping: App\Presentation\*\**Presenter +``` + +Wie funktioniert das Mapping? Um das besser zu verstehen, stellen wir uns zunächst eine Anwendung ohne Module vor. Wir möchten, dass die Presenter-Klassen in den Namensraum `App\Presentation` fallen, so dass der Presenter `Home` auf die Klasse `App\Presentation\HomePresenter` abgebildet wird. Dies wird mit dieser Konfiguration erreicht: + +```neon +application: + mapping: App\Presentation\*Presenter +``` + +Das Mapping funktioniert, indem das Sternchen in der Maske `App\Presentation\*Presenter` durch den Presenter-Namen `Home` ersetzt wird, was zu dem endgültigen Klassennamen `App\Presentation\HomePresenter` führt. Einfach! + +Wie Sie jedoch in den Beispielen in diesem und anderen Kapiteln sehen, platzieren wir Presenter-Klassen in gleichnamigen Unterverzeichnissen, z. B. wird der Presenter `Home` der Klasse `App\Presentation\Home\HomePresenter` zugeordnet. Wir erreichen dies, indem wir den Doppelpunkt verdoppeln (erfordert Nette Application 3.2): + +```neon +application: + mapping: App\Presentation\**Presenter +``` + +Nun gehen wir dazu über, Presenter in Modulen abzubilden. Wir können für jedes Modul eine spezifische Zuordnung definieren: + +```neon +application: + mapping: + Front: App\Presentation\Front\**Presenter + Admin: App\Presentation\Admin\**Presenter + Api: App\Api\*Presenter +``` + +Nach dieser Konfiguration wird der Präsentator `Front:Home` der Klasse `App\Presentation\Front\Home\HomePresenter` zugeordnet, während der Präsentator `Api:OAuth` der Klasse `App\Api\OAuthPresenter` zugeordnet wird. + +Da die Module `Front` und `Admin` eine ähnliche Zuordnungsmethode haben und es wahrscheinlich noch mehr solcher Module geben wird, ist es möglich, eine allgemeine Regel zu erstellen, die sie ersetzen wird. Ein neues Sternchen für das Modul wird der Klassenmaske hinzugefügt: + +```neon +application: + mapping: + *: App\Presentation\*\**Presenter + Api: App\Api\*Presenter +``` + +Dies funktioniert auch für tiefer verschachtelte Verzeichnisstrukturen, wie z. B. Presenter `Admin:User:Edit`, wo das Segment mit Sternchen für jede Ebene wiederholt wird und die Klasse `App\Presentation\Admin\User\Edit\EditPresenter` ergibt. + +Eine alternative Schreibweise ist die Verwendung eines Arrays, das aus drei Segmenten anstelle einer Zeichenkette besteht. Diese Notation ist äquivalent zur vorherigen: + +```neon +application: + mapping: + *: [App\Presentation, *, **Presenter] + Api: [App\Api, '', *Presenter] +``` diff --git a/application/de/how-it-works.texy b/application/de/how-it-works.texy index 3ac55697e4..481efa71d8 100644 --- a/application/de/how-it-works.texy +++ b/application/de/how-it-works.texy @@ -22,18 +22,18 @@ Die Verzeichnisstruktur sieht in etwa so aus: /--pre web-project/ ├── app/ ← Verzeichnis mit Anwendung -│ ├── Presenters/ ← Presenter-Klassen -│ │ ├── HomePresenter.php ← Home presenterklasse -│ │ └── templates/ ← Vorlagenverzeichnis -│ │ ├── @layout.latte ← Vorlage für gemeinsames Layout -│ │ └── Home/ ← Vorlagen für Home-presenter -│ │ └── default.latte ← Vorlage für Aktion `default` -│ ├── Router/ ← Konfiguration von URL-Adressen +│ ├── Core/ ← grundlegende notwendige Klassen +│ │ └── RouterFactory.php ← Konfiguration der URL-Adressen +│ ├── Presentation/ ← Moderatoren, Vorlagen & Co. +│ │ ├── @layout.latte ← Vorlage für gemeinsames Layout +│ │ └── Home/ ← Home Presenter Verzeichnis +│ │ ├── HomePresenter.php ← Home Presenter Klasse +│ │ └── default.latte ← Vorlage für Aktion default │ └── Bootstrap.php ← bootende Klasse Bootstrap ├── bin/ ← Skripte für die Kommandozeile ├── config/ ← Konfigurationsdateien │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← Fehlerprotokolle ├── temp/ ← temporäre Dateien, Cache, … ├── vendor/ ← vom Composer installierte Bibliotheken @@ -45,9 +45,9 @@ Die Verzeichnisstruktur sieht in etwa so aus: └── .htaccess ← verbietet den Zugriff auf alle Verzeichnisse außer www \-- -Sie können die Verzeichnisstruktur beliebig ändern, Ordner umbenennen oder verschieben, und dann einfach die Pfade zu `log/` und `temp/` in der Datei `Bootstrap.php` und den Pfad zu dieser Datei in `composer.json` im Abschnitt `autoload` ändern. Nichts weiter, keine komplizierte Neukonfiguration, keine ständigen Änderungen. Nette hat eine [intelligente automatische Erkennung |bootstrap#development-vs-production-mode]. +Sie können die Verzeichnisstruktur nach Belieben ändern, Ordner umbenennen oder verschieben - es ist völlig flexibel. Nette verfügt außerdem über eine intelligente automatische Erkennung und erkennt automatisch den Speicherort der Anwendung einschließlich ihrer URL-Basis. -Für etwas größere Anwendungen können wir Ordner mit Präsentatoren und Vorlagen in Unterverzeichnisse (auf der Festplatte) und in Namensräume (im Code) unterteilen, die wir [Module |modules] nennen. +Bei etwas größeren Anwendungen können wir Präsentations- und Vorlagenordner in [Unterverzeichnissen |directory-structure#Presenters and templates] organisieren und Klassen in Namensräumen gruppieren, die wir Module nennen. Das Verzeichnis `www/` ist das öffentliche Verzeichnis oder die Dokumenten-Wurzel des Projekts. Sie können es umbenennen, ohne etwas anderes auf der Anwendungsseite einstellen zu müssen. Sie müssen nur [das Hosting |nette:troubleshooting#How to change or remove www directory from URL] so [konfigurieren |nette:troubleshooting#How to change or remove www directory from URL], dass die Dokumentenwurzel in dieses Verzeichnis führt. @@ -75,7 +75,7 @@ Seine Aufgabe ist: Welche Art von Fabrik? Wir stellen keine Traktoren her, sondern Websites! Warten Sie, ich erkläre es Ihnen gleich. -Mit "die Umgebung initialisieren" meinen wir zum Beispiel, dass [Tracy |tracy:] aktiviert wird, ein erstaunliches Werkzeug zur Protokollierung oder Visualisierung von Fehlern. Es protokolliert Fehler auf dem Produktionsserver und zeigt sie direkt auf dem Entwicklungsserver an. Daher muss bei der Initialisierung auch entschieden werden, ob die Site im Produktions- oder im Entwicklermodus läuft. Hierfür verwendet Nette eine automatische Erkennung: Wenn Sie die Site auf localhost ausführen, läuft sie im Entwicklermodus. Sie müssen nichts konfigurieren, und die Anwendung ist sowohl für die Entwicklung als auch für den Produktionseinsatz bereit. Diese Schritte werden im Kapitel über die [Bootstrap-Klasse |bootstrap] durchgeführt und ausführlich beschrieben. +Mit "Umgebungsinitialisierung" meinen wir z. B. die Aktivierung von [Tracy |tracy:], einem fantastischen Werkzeug für die Protokollierung und Fehlervisualisierung. Auf Produktionsservern protokolliert es Fehler, während es sie auf Entwicklungsservern direkt anzeigt. Zur Initialisierung gehört also auch die Festlegung, ob die Website im Produktions- oder Entwicklungsmodus läuft. Hierfür verwendet Nette eine [intelligente automatische Erkennung |bootstrap#development-vs-production-mode]: Wenn Sie die Website auf localhost ausführen, läuft sie im Entwicklungsmodus. Es ist keine Konfiguration erforderlich, und die Anwendung ist sowohl für die Entwicklung als auch für den Produktionseinsatz bereit. Diese Schritte werden im Kapitel über die [Bootstrap-Klassen |bootstrap] durchgeführt und ausführlich beschrieben. Der dritte Punkt (ja, wir haben den zweiten übersprungen, aber wir werden darauf zurückkommen) ist das Starten der Anwendung. Die Bearbeitung von HTTP-Anfragen in Nette erfolgt durch die Klasse `Nette\Application\Application` (im Folgenden `Application` genannt). Wenn wir also sagen "eine Anwendung starten", meinen wir den Aufruf einer Methode mit dem Namen `run()` auf einem Objekt dieser Klasse. @@ -91,7 +91,7 @@ In Nette geschriebene Anwendungen sind in viele so genannte Presenter unterteilt Die Anwendung beginnt damit, dass sie den so genannten Router bittet, zu entscheiden, an welchen der Presenter die aktuelle Anfrage zur Bearbeitung weitergeleitet werden soll. Der Router entscheidet, wer dafür zuständig ist. Er sieht sich die Eingabe-URL `https://example.com/product/123` handelt, der ein Produkt mit `id: 123` als Aktion an `show` weiterleiten möchte. Es ist eine gute Angewohnheit, ein durch einen Doppelpunkt getrenntes Paar aus Präsentator + Aktion als `Product:show` zu schreiben. -Der Router verwandelt also die URL in ein Paar `Presenter:action` + Parameter, in unserem Fall `Product:show` + `id: 123`. Sie können sehen, wie ein Router in der Datei `app/Router/RouterFactory.php` aussieht, und wir werden ihn im Kapitel [Routing] ausführlich beschreiben. +Der Router verwandelt also die URL in ein Paar `Presenter:action` + Parameter, in unserem Fall `Product:show` + `id: 123`. Sie können sehen, wie ein Router in der Datei `app/Core/RouterFactory.php` aussieht, und wir werden ihn im Kapitel [Routing] ausführlich beschreiben. Machen wir weiter. Die Anwendung kennt bereits den Namen des Präsentators und kann fortfahren. Sie erstellt ein Objekt `ProductPresenter`, das den Code des Presenters `Product` darstellt. Genauer gesagt, sie bittet den DI-Container um die Erstellung des Presenters, denn die Erstellung von Objekten ist seine Aufgabe. @@ -121,12 +121,9 @@ So wurde die Methode `renderShow(123)` aufgerufen, deren Code ein fiktives Beisp Anschließend gibt der Präsentator die Antwort zurück. Dies kann eine HTML-Seite, ein Bild, ein XML-Dokument, das Senden einer Datei von der Festplatte, JSON oder die Routing zu einer anderen Seite sein. Wichtig ist, dass, wenn wir nicht ausdrücklich sagen, wie zu antworten ist (was bei `ProductPresenter` der Fall ist), die Antwort darin besteht, die Vorlage mit einer HTML-Seite wiederzugeben. Und warum? Nun, weil wir in 99 % der Fälle eine Vorlage zeichnen wollen, so dass der Präsentator dieses Verhalten als Standard annimmt und uns die Arbeit erleichtern will. Das ist der Punkt von Nette. -Wir müssen nicht einmal angeben, welche Vorlage gezeichnet werden soll, er leitet den Pfad dorthin nach einer einfachen Logik ab. Im Fall von presenter `Product` und action `show` versucht er zu sehen, ob eine dieser Vorlagendateien relativ zu dem Verzeichnis existiert, in dem sich die Klasse `ProductPresenter` befindet: +Wir müssen nicht einmal angeben, welche Vorlage gerendert werden soll; das Framework wird den Pfad selbst ermitteln. Im Fall der Aktion `show` versucht es einfach, die Vorlage `show.latte` im Verzeichnis mit der Klasse `ProductPresenter` zu laden. Es versucht auch, das Layout in der Datei `@layout.latte` zu finden (mehr über die [Vorlagensuche |templates#Template Lookup]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -Außerdem wird versucht, das Layout in der Datei `@layout.latte` zu finden, und dann wird die Vorlage gerendert. Nun ist die Aufgabe des Präsentators und der gesamten Anwendung abgeschlossen. Wenn die Vorlage nicht existiert, wird eine Seite mit dem Fehler 404 zurückgegeben. Weitere Informationen über Präsentatoren finden Sie auf der Seite [Präsentatoren |Presenters]. +Anschließend werden die Vorlagen gerendert. Damit ist die Aufgabe des Präsentators und der gesamten Anwendung abgeschlossen, und die Arbeit ist getan. Wenn die Vorlage nicht vorhanden wäre, würde eine 404-Fehlerseite zurückgegeben werden. Weitere Informationen über Präsentatoren finden Sie auf der Seite [Präsentatoren |presenters]. [* request-flow.svg *] @@ -137,7 +134,7 @@ Um sicherzugehen, versuchen wir, den gesamten Prozess mit einer etwas anderen UR 3) der Router dekodiert die URL als ein Paar `Home:default` 4) ein `HomePresenter` Objekt wird erstellt 5) die Methode `renderDefault()` wird aufgerufen (falls vorhanden) -6) eine Vorlage `templates/Home/default.latte` mit einem Layout `templates/@layout.latte` wird gerendert +6) eine Vorlage `default.latte` mit einem Layout `@layout.latte` wird gerendert Vielleicht sind Sie jetzt auf eine Menge neuer Konzepte gestoßen, aber wir glauben, dass sie sinnvoll sind. Das Erstellen von Anwendungen in Nette ist ein Kinderspiel. diff --git a/application/de/modules.texy b/application/de/modules.texy deleted file mode 100644 index fcdf4331c4..0000000000 --- a/application/de/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Module -****** - -.[perex] -In Nette stellen Module die logischen Einheiten dar, aus denen eine Anwendung besteht. Sie umfassen Presenter, Templates, eventuell auch Komponenten und Modellklassen. - -Ein Verzeichnis für Presenter und eines für Templates würde für echte Projekte nicht ausreichen. Dutzende von Dateien in einem Ordner zu haben, ist zumindest unorganisiert. Wie kommt man da wieder raus? Wir teilen sie einfach in Unterverzeichnisse auf der Festplatte und in Namensräume im Code auf. Und das ist genau das, was die Nette-Module tun. - -Vergessen wir also einen einzigen Ordner für Präsentatoren und Vorlagen und erstellen wir stattdessen Module, zum Beispiel `Admin` und `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← Verzeichnis mit Modulen -│ ├── Admin/ ← Modul Admin -│ │ ├── Presenters/ ← seine Presenters -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← Modul Front -│ └── Presenters/ ← seine Presenters -│ └── ... -\-- - -Diese Verzeichnisstruktur spiegelt sich in den Klassennamensräumen wider, so dass z. B. `DashboardPresenter` im Namensraum `App\Modules\Admin\Presenters` liegt: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Der Präsentator `Dashboard` innerhalb des Moduls `Admin` wird innerhalb der Anwendung mit der Doppelpunktschreibweise als `Admin:Dashboard` referenziert, und seine Aktion `default` als `Admin:Dashboard:default`. -Und woher weiß Nette selbst, dass `Admin:Dashboard` die Klasse `App\Modules\Admin\Presenters\DashboardPresenter` repräsentiert? Dies wird durch ein [Mapping |#mapping] in der Konfiguration festgelegt. -Die vorgegebene Struktur ist also nicht fest vorgegeben und kann nach Belieben verändert werden. - -Module können neben Presentern und Templates natürlich auch alle anderen Elemente enthalten, wie z.B. Komponenten, Modellklassen, etc. - - -Verschachtelte Module .[#toc-nested-modules] --------------------------------------------- - -Module müssen nicht nur eine flache Struktur bilden, Sie können auch Untermodule erstellen, zum Beispiel: - -/--pre -app/ -├── Modules/ ← Verzeichnis mit Modulen -│ ├── Blog/ ← Modul Blog -│ │ ├── Admin/ ← Submodul Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← Submodul Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← Modul Forum -│ │ └── ... -\-- - -So wird das Modul `Blog` in die Untermodule `Admin` und `Front` aufgeteilt. Dies spiegelt sich auch in den Namensräumen wider, die dann `App\Modules\Blog\Admin\Presenters` usw. lauten. Der Präsentator `Dashboard` innerhalb des Submoduls wird als `Blog:Admin:Dashboard` bezeichnet. - -Die Verschachtelung kann beliebig tief gehen, so dass Sub-Submodule erstellt werden können. - - -Erstellen von Links .[#toc-creating-links] ------------------------------------------- - -Links in Präsentatorvorlagen sind relativ zum aktuellen Modul. So führt der Link `Foo:default` zu dem Präsentator `Foo` im gleichen Modul wie der aktuelle Präsentator. Wenn das aktuelle Modul zum Beispiel `Front` ist, sieht der Link so aus: - -```latte -link to Front:Product:show -``` - -Ein Link ist auch dann relativ, wenn er den Namen eines Moduls enthält, das dann als Untermodul betrachtet wird: - -```latte -link to Front:Shop:Product:show -``` - -Absolute Links werden analog zu absoluten Pfaden auf der Festplatte geschrieben, jedoch mit Doppelpunkten anstelle von Schrägstrichen. Ein absoluter Link beginnt also mit einem Doppelpunkt: - -```latte -link to Admin:Product:show -``` - -Um herauszufinden, ob wir uns in einem bestimmten Modul oder dessen Untermodul befinden, können wir die Funktion `isModuleCurrent(moduleName)` verwenden. - -```latte -
  • - ... -
  • -``` - - -Routing .[#toc-routing] ------------------------ - -Siehe [Kapitel über Routing |routing#Modules]. - - -Abbildung .[#toc-mapping] -------------------------- - -Legt die Regeln fest, nach denen der Klassenname aus dem Namen des Präsentators abgeleitet wird. Wir schreiben sie in die [Konfiguration |configuration] unter dem Schlüssel `application › mapping`. - -Beginnen wir mit einem Beispiel, das keine Module verwendet. Wir wollen nur, dass die Presenter-Klassen den Namespace `App\Presenters` haben. Das bedeutet, dass ein Presenter wie `Home` auf die Klasse `App\Presenters\HomePresenter` abgebildet werden soll. Dies kann durch die folgende Konfiguration erreicht werden: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -Der Name des Presenters wird durch das Sternchen in der Klassenmaske ersetzt und das Ergebnis ist der Klassenname. Einfach! - -Wenn wir die Vortragenden in Module unterteilen, können wir für jedes Modul eine eigene Zuordnung vornehmen: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Der Referent `Front:Home` wird der Klasse `App\Modules\Front\Presenters\HomePresenter` zugeordnet und der Referent `Admin:Dashboard` der Klasse `App\Modules\Admin\Presenters\DashboardPresenter`. - -Es ist praktischer, eine allgemeine (Stern-)Regel zu erstellen, um die ersten beiden zu ersetzen. Das zusätzliche Sternchen wird der Klassenmaske nur für dieses Modul hinzugefügt: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Was aber, wenn wir verschachtelte Module verwenden und einen Präsentator `Admin:User:Edit` haben? In diesem Fall wird das Segment mit dem Sternchen, das das Modul für jede Ebene darstellt, einfach wiederholt und das Ergebnis ist die Klasse `App\Modules\Admin\User\Presenters\EditPresenter`. - -Eine alternative Schreibweise ist die Verwendung eines Arrays, das aus drei Segmenten anstelle einer Zeichenkette besteht. Diese Notation ist gleichwertig mit der vorherigen: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -Der Standardwert ist `*: *Module\*Presenter`. diff --git a/application/de/presenters.texy b/application/de/presenters.texy index 35bc4c3513..8622224006 100644 --- a/application/de/presenters.texy +++ b/application/de/presenters.texy @@ -60,7 +60,7 @@ Unmittelbar nach Erhalt der Anfrage wird die Methode `startup ()` aufgerufen. Si Es ist wichtig, dass `action()` vor aufgerufen wird `render()`aufgerufen wird, damit wir darin möglicherweise den weiteren Verlauf des Lebenszyklus ändern können, d. h. die Vorlage, die gerendert wird, und auch die Methode `render()` die aufgerufen wird, mit `setView('otherView')`. -Die Parameter der Anfrage werden an die Methode übergeben. Es ist möglich und empfehlenswert, Typen für die Parameter anzugeben, z. B. `actionShow(int $id, string $slug = null)` - wenn der Parameter `id` fehlt oder keine ganze Zahl ist, gibt der Präsentator den [Fehler 404 |#Error 404 etc.] zurück und bricht die Operation ab. +Die Parameter der Anfrage werden an die Methode übergeben. Es ist möglich und empfehlenswert, Typen für die Parameter anzugeben, z. B. `actionShow(int $id, ?string $slug = null)` - wenn der Parameter `id` fehlt oder keine ganze Zahl ist, gibt der Präsentator den [Fehler 404 |#Error 404 etc.] zurück und bricht die Operation ab. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ In der Vorlage sind diese Meldungen in der Variablen `$flashes` als Objekte `std Fehler 404 usw. .[#toc-error-404-etc] ===================================== -Wenn wir die Anfrage nicht erfüllen können, weil z.B. der Artikel, den wir anzeigen wollen, nicht in der Datenbank existiert, werden wir den Fehler 404 mit der Methode `error(string $message = null, int $httpCode = 404)` ausgeben, die den HTTP-Fehler 404 darstellt: +Wenn wir die Anfrage nicht erfüllen können, weil z.B. der Artikel, den wir anzeigen wollen, nicht in der Datenbank existiert, werden wir den Fehler 404 mit der Methode `error(?string $message = null, int $httpCode = 404)` ausgeben, die den HTTP-Fehler 404 darstellt: ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Parameter anfordern .[#toc-request-parameters] +============================================== + +Der Präsentator, wie auch jede Komponente, bezieht seine Parameter aus der HTTP-Anfrage. Ihre Werte können mit der Methode `getParameter($name)` oder `getParameters()` abgerufen werden. Bei den Werten handelt es sich um Strings oder Arrays von Strings, also im Wesentlichen um Rohdaten, die direkt aus der URL bezogen werden. + +Für zusätzlichen Komfort empfiehlt es sich, die Parameter über Eigenschaften zugänglich zu machen. Beschriften Sie sie einfach mit dem `#[Parameter]` Attribut: + +```php +use Nette\Application\Attributes\Parameter; // diese Zeile ist wichtig + +class HomePresenter extends Nette\Application\UI\Presenter +{ + #[Parameter] + public string $theme; // muss öffentlich sein +} +``` + +Für Eigenschaften empfehlen wir die Angabe des Datentyps (z. B. `string`). Nette wird den Wert dann automatisch auf der Grundlage dieses Typs umwandeln. Parameterwerte können auch [validiert |#Validation of Parameters] werden. + +Beim Erstellen einer Verknüpfung können Sie den Wert für die Parameter direkt festlegen: + +```latte +click +``` + + Dauerhafte Parameter .[#toc-persistent-parameters] ================================================== @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Wenn `$this->lang` einen Wert wie `'en'` hat, dann werden Links, die mit `link()` oder `n:href` erstellt werden, auch den Parameter `lang=en` enthalten. Und wenn der Link angeklickt wird, wird er wieder `$this->lang = 'en'` sein. -Für Eigenschaften wird empfohlen, den Datentyp anzugeben (z. B. `string`), und Sie können auch einen Standardwert angeben. Parameterwerte können [validiert |#Validation of Persistent Parameters] werden. +Für Eigenschaften wird empfohlen, den Datentyp anzugeben (z. B. `string`), und Sie können auch einen Standardwert angeben. Parameterwerte können [validiert |#Validation of Parameters] werden. Persistente Parameter werden standardmäßig zwischen allen Aktionen eines bestimmten Präsentators weitergegeben. Um sie zwischen mehreren Präsentatoren zu übergeben, müssen Sie sie entweder definieren: @@ -307,18 +333,12 @@ Tiefer gehen .[#toc-going-deeper] Was wir bisher in diesem Kapitel gezeigt haben, wird wahrscheinlich ausreichen. Die folgenden Zeilen sind für diejenigen gedacht, die sich eingehend mit Moderatoren beschäftigen und alles wissen wollen. -Anforderung und Parameter .[#toc-requirement-and-parameters] ------------------------------------------------------------- - -Die vom Präsentator bearbeitete Anfrage ist das Objekt [api:Nette\Application\Request] und wird von der Methode `getRequest()` des Präsentators zurückgegeben. Sie enthält ein Array von Parametern, und jeder von ihnen gehört entweder zu einer der Komponenten oder direkt zum Präsentator (der eigentlich auch eine Komponente ist, wenn auch eine spezielle). Nette verteilt also die Parameter um und übergibt sie zwischen den einzelnen Komponenten (und dem Präsentator) durch Aufruf der Methode `loadState(array $params)`. Die Parameter können mit der Methode `getParameters(): array`, einzeln mit `getParameter($name)` abgerufen werden. Bei den Parameterwerten handelt es sich um Strings oder Arrays von Strings, also im Grunde um Rohdaten, die direkt aus einer URL bezogen werden. +Validierung von Parametern .[#toc-validation-of-parameters] +----------------------------------------------------------- +Die Werte von [Anfrageparametern |#request parameters] und [dauerhaften Parametern |#persistent parameters], die von URLs empfangen werden, werden von der Methode `loadState()` in Eigenschaften geschrieben. Sie prüft auch, ob der in der Eigenschaft angegebene Datentyp übereinstimmt, andernfalls antwortet sie mit einem 404-Fehler und die Seite wird nicht angezeigt. -Validierung von persistenten Parametern .[#toc-validation-of-persistent-parameters] ------------------------------------------------------------------------------------ - -Die Werte von [persistenten Parametern |#persistent parameters], die von URLs empfangen werden, werden von der Methode `loadState()` in Eigenschaften geschrieben. Sie prüft auch, ob der in der Eigenschaft angegebene Datentyp übereinstimmt, andernfalls antwortet sie mit einem 404-Fehler und die Seite wird nicht angezeigt. - -Verlassen Sie sich niemals blind auf persistente Parameter, da sie leicht vom Benutzer in der URL überschrieben werden können. So überprüfen wir zum Beispiel, ob `$this->lang` zu den unterstützten Sprachen gehört. Eine gute Möglichkeit, dies zu tun, besteht darin, die oben erwähnte Methode `loadState()` zu überschreiben: +Verlassen Sie sich niemals blind auf Parameter, da sie leicht vom Benutzer in der URL überschrieben werden können. So überprüfen wir zum Beispiel, ob `$this->lang` zu den unterstützten Sprachen gehört. Eine gute Möglichkeit, dies zu tun, besteht darin, die oben erwähnte Methode `loadState()` zu überschreiben: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,7 +361,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Speichern und Wiederherstellen der Anfrage .[#toc-save-and-restore-the-request] ------------------------------------------------------------------------------- -Sie können die aktuelle Anfrage in einer Session speichern oder sie aus der Session wiederherstellen und den Präsentator sie erneut ausführen lassen. Dies ist z. B. nützlich, wenn ein Benutzer ein Formular ausfüllt und seine Anmeldung abläuft. Um keine Daten zu verlieren, speichern wir vor der Routing zur Anmeldeseite die aktuelle Anfrage in der Session mit `$reqId = $this->storeRequest()`, die einen Bezeichner in Form einer kurzen Zeichenkette zurückgibt und ihn als Parameter an den Anmeldepräsentator übergibt. +Die Anfrage, die der Präsentator bearbeitet, ist ein Objekt [api:Nette\Application\Request] und wird von der Methode `getRequest()` des Präsentators zurückgegeben. + +Sie können die aktuelle Anfrage in einer Sitzung speichern oder sie aus der Sitzung wiederherstellen und den Präsentator sie erneut ausführen lassen. Dies ist z. B. nützlich, wenn ein Benutzer ein Formular ausfüllt und sein Login abläuft. Um keine Daten zu verlieren, speichern wir vor der Weiterleitung zur Anmeldeseite die aktuelle Anfrage in der Sitzung mit `$reqId = $this->storeRequest()`, die einen Bezeichner in Form eines kurzen Strings zurückgibt und diesen als Parameter an den Anmeldepräsentator übergibt. Nach der Anmeldung rufen wir die Methode `$this->restoreRequest($reqId)` auf, die die Anfrage aus der Session abholt und an diese weiterleitet. Die Methode prüft, ob die Anfrage von demselben Benutzer erstellt wurde, der jetzt angemeldet ist. Wenn sich ein anderer Benutzer anmeldet oder der Schlüssel ungültig ist, tut sie nichts und das Programm läuft weiter. @@ -362,7 +384,7 @@ Eine Umleitung findet bei einer AJAX- oder POST-Anfrage nicht statt, da dies zu Sie können die Kanonisierung auch manuell mit der Methode `canonicalize()` aufrufen, die wie die Methode `link()` den Präsentator, Aktionen und Parameter als Argumente erhält. Sie erstellt einen Link und vergleicht ihn mit der aktuellen URL. Wenn sie sich unterscheidet, wird sie auf den erzeugten Link umgeleitet. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // leitet um, wenn $slug nicht mit $realSlug übereinstimmt @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Zugangsbeschränkung mit `#[Requires]` .[#toc-access-restriction-using-requires]{data-version:3.2.2} +--------------------------------------------------------------------------------------------------- + +Das Attribut `#[Requires]` Attribut bietet erweiterte Optionen zur Einschränkung des Zugriffs auf Präsentatoren und ihre Methoden. Es kann verwendet werden, um HTTP-Methoden zu spezifizieren, AJAX-Anfragen zu verlangen, den Zugriff auf denselben Ursprung zu beschränken und den Zugriff nur auf Weiterleitungen zu beschränken. Das Attribut kann sowohl auf Presenter-Klassen als auch auf einzelne Methoden angewendet werden, z. B. `action()`, `render()`, `handle()`, und `createComponent()`. + +Sie können diese Einschränkungen angeben: +- auf HTTP-Methoden: `#[Requires(methods: ['GET', 'POST'])]` +- die eine AJAX-Anfrage erfordern: `#[Requires(ajax: true)]` +- Zugriff nur vom selben Ursprung: `#[Requires(sameOrigin: true)]` +- Zugriff nur über Weiterleitung: `#[Requires(forward: true)]` +- Einschränkungen für bestimmte Aktionen: `#[Requires(actions: 'default')]` + +Für Einzelheiten siehe [Verwendung des Requires Attributs |best-practices:attribute-requires]. + + +HTTP-Methodenprüfung .[#toc-http-method-check] +---------------------------------------------- + +In Nette überprüfen die Moderatoren die HTTP-Methode jeder eingehenden Anfrage automatisch, hauptsächlich aus Sicherheitsgründen. Standardmäßig sind die Methoden `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` zugelassen. + +Wenn Sie zusätzliche Methoden wie `OPTIONS` aktivieren möchten, können Sie das `#[Requires]` Attribut verwenden (ab Nette Application v3.2): + +```php +#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] +class MyPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +In Version 3.1 wird die Überprüfung in `checkHttpMethod()` durchgeführt, das prüft, ob die in der Anfrage angegebene Methode im Array `$presenter->allowedMethods` enthalten ist. Fügen Sie eine Methode wie diese hinzu: + +```php +class MyPresenter extends Nette\Application\UI\Presenter +{ + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } +} +``` + +Es ist wichtig zu betonen, dass, wenn Sie die `OPTIONS` -Methode zulassen, Sie sie auch in Ihrem Präsentator richtig behandeln müssen. Diese Methode wird häufig als so genannte Preflight-Anfrage verwendet, die von Browsern automatisch vor der eigentlichen Anfrage gesendet wird, wenn es darum geht, festzustellen, ob die Anfrage aus Sicht der CORS-Richtlinie (Cross-Origin Resource Sharing) zulässig ist. Wenn Sie diese Methode zulassen, aber keine angemessene Antwort implementieren, kann dies zu Inkonsistenzen und potenziellen Sicherheitsproblemen führen. + + Weitere Lektüre .[#toc-further-reading] ======================================= diff --git a/application/de/routing.texy b/application/de/routing.texy index dac42c271d..c60b743cb3 100644 --- a/application/de/routing.texy +++ b/application/de/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Oder wir können diese Form verwenden, beachten Sie die Umschreibung des regulären Ausdrucks für die Validierung: +Für eine detailliertere Spezifikation kann eine noch umfangreichere Form verwendet werden, bei der zusätzlich zu den Standardwerten weitere Parametereigenschaften festgelegt werden können, wie z. B. ein regulärer Ausdruck für die Validierung (siehe den Parameter `id` ): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Diese gesprächigeren Formate sind nützlich, um andere Metadaten hinzuzufügen. +Es ist wichtig zu beachten, dass, wenn die im Array definierten Parameter nicht in der Pfadmaske enthalten sind, ihre Werte nicht geändert werden können, auch nicht mit Abfrageparametern, die nach einem Fragezeichen in der URL angegeben werden. Filter und Übersetzungen .[#toc-filters-and-translations] @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Module .[#toc-modules] ---------------------- -Wenn wir mehrere Routen haben, die zu einem [Modul |modules] gehören, können wir `withModule()` verwenden, um sie zu gruppieren: +Wenn wir mehrere Routen haben, die zu einem [Modul |directory-structure#Presenters and Templates] gehören, können wir `withModule()` verwenden, um sie zu gruppieren: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Einbindung .[#toc-integration] ============================== -Um unseren Router in die Anwendung einzubinden, müssen wir ihn dem DI-Container mitteilen. Am einfachsten ist es, die Fabrik vorzubereiten, die das Router-Objekt erstellt, und der Container-Konfiguration mitzuteilen, dass sie es verwenden soll. Schreiben wir also eine Methode für diesen Zweck `App\Router\RouterFactory::createRouter()`: +Um unseren Router in die Anwendung einzubinden, müssen wir ihn dem DI-Container mitteilen. Am einfachsten ist es, die Fabrik vorzubereiten, die das Router-Objekt erstellt, und der Container-Konfiguration mitzuteilen, dass sie es verwenden soll. Schreiben wir also eine Methode für diesen Zweck `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ Dann schreiben wir in [configuration |dependency-injection:services]: ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Alle Abhängigkeiten, wie z. B. eine Datenbankverbindung usw., werden der Factory-Methode als Parameter über [Autowiring |dependency-injection:autowiring] übergeben: @@ -663,7 +663,7 @@ Unter getrennter Nutzung verstehen wir die Verwendung der Router-Funktionen in e Wir werden also wieder eine Methode erstellen, die einen Router aufbaut, zum Beispiel: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Oder wir erstellen die Objekte direkt: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/de/templates.texy b/application/de/templates.texy index a21bd7266f..9af8b475a7 100644 --- a/application/de/templates.texy +++ b/application/de/templates.texy @@ -34,35 +34,81 @@ Und dies könnte die Aktionsvorlage sein: Sie definiert den Block `content`, der anstelle von `{include content}` in das Layout eingefügt wird, und definiert auch den Block `title` neu, der `{block title}` im Layout überschreibt. Versuchen Sie, sich das Ergebnis vorzustellen. -Suche nach Templates .[#toc-search-for-templates] -------------------------------------------------- +Vorlage nachschlagen .[#toc-template-lookup] +-------------------------------------------- + +In Presentern müssen Sie nicht angeben, welche Vorlage gerendert werden soll; das Framework bestimmt den Pfad automatisch, was die Codierung für Sie einfacher macht. + +Wenn Sie eine Verzeichnisstruktur verwenden, in der jeder Präsentator sein eigenes Verzeichnis hat, legen Sie die Vorlage einfach in diesem Verzeichnis unter dem Namen der Aktion (d. h. der Ansicht) ab. Verwenden Sie zum Beispiel für die Aktion `default` die Vorlage `default.latte`: -Der Pfad zu den Vorlagen wird nach einer einfachen Logik hergeleitet. Es wird versucht zu sehen, ob eine dieser Vorlagendateien relativ zu dem Verzeichnis existiert, in dem sich die Presenter-Klasse befindet, wobei `` der Name des aktuellen Präsentators ist und `` der Name der aktuellen Aktion ist: +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -- `templates//.latte` -- `templates/..latte` +Wenn Sie eine Struktur verwenden, bei der sich die Präsentatoren in einem Verzeichnis und die Vorlagen in einem Ordner `templates` befinden, speichern Sie sie entweder in einer Datei `..latte` oder `/.latte`: -Wird die Vorlage nicht gefunden, wird versucht, im Verzeichnis `templates` eine Ebene höher zu suchen, d. h. auf der gleichen Ebene wie das Verzeichnis mit der Presenter-Klasse. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- -Wenn die Vorlage auch dort nicht gefunden wird, ist die Antwort ein [404-Fehler |presenters#Error 404 etc.]. +Das Verzeichnis `templates` kann auch eine Ebene höher platziert werden, auf derselben Ebene wie das Verzeichnis mit den Presenter-Klassen. -Sie können die Ansicht auch mit `$this->setView('otherView')` ändern. Oder geben Sie statt der Suche direkt den Namen der Vorlagendatei mit `$this->template->setFile('/path/to/template.latte')` an. +Wenn die Vorlage nicht gefunden wird, antwortet der Präsentator mit dem [Fehler 404 - Seite nicht gefunden |presenters#Error 404 etc]. + +Sie können die Ansicht mit `$this->setView('anotherView')` ändern. Es ist auch möglich, die Vorlagendatei direkt mit `$this->template->setFile('/path/to/template.latte')` anzugeben. .[note] -Sie können die Pfade, in denen Vorlagen gesucht werden, ändern, indem Sie die Methode [formatTemplateFiles |api:Nette\Application\UI\Presenter::formatTemplateFiles()] überschreiben, die ein Array mit möglichen Dateipfaden zurückgibt. +Dateien, in denen Vorlagen gesucht werden, können durch Überschreiben der Methode [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()] geändert werden, die ein Array mit möglichen Dateinamen zurückgibt. + + +Layout-Vorlagen-Suche .[#toc-layout-template-lookup] +---------------------------------------------------- + +Nette sucht auch automatisch nach der Layout-Datei. + +Wenn Sie eine Verzeichnisstruktur verwenden, in der jeder Präsentator sein eigenes Verzeichnis hat, legen Sie das Layout entweder in dem Ordner mit dem Präsentator ab, wenn es nur für diesen spezifisch ist, oder eine Ebene höher, wenn es für mehrere Präsentatoren gemeinsam ist: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +Wenn Sie eine Struktur verwenden, bei der die Vortragenden in einem Verzeichnis zusammengefasst sind und sich die Vorlagen in einem Ordner `templates` befinden, wird das Layout an den folgenden Stellen erwartet: -Das Layout wird in den folgenden Dateien erwartet: +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` gemeinsames Layout für mehrere Präsentatoren +Befindet sich der Präsentator in einem Modul, wird er auch weiter oben im Verzeichnisbaum entsprechend der Verschachtelung des Moduls gesucht. -`` ist der Name des aktuellen Präsentators und `` ist der Name des Layouts, der standardmäßig `'layout'` lautet. Der Name kann mit `$this->setLayout('otherLayout')` geändert werden, so dass `@otherLayout.latte` Dateien ausprobiert werden. +Der Name des Layouts kann mit `$this->setLayout('layoutAdmin')` geändert werden und wird dann in der Datei `@layoutAdmin.latte` erwartet. Sie können die Layout-Vorlagendatei auch direkt mit `$this->setLayout('/path/to/template.latte')` angeben. -Sie können auch direkt den Dateinamen der Layoutvorlage mit `$this->setLayout('/path/to/template.latte')` angeben. Durch die Verwendung von `$this->setLayout(false)` wird die Layout-Suche deaktiviert. +Die Verwendung von `$this->setLayout(false)` oder des Tags `{layout none}` innerhalb der Vorlage deaktiviert die Layout-Suche. .[note] -Sie können die Pfade, in denen Vorlagen gesucht werden, ändern, indem Sie die Methode [formatLayoutTemplateFiles |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] überschreiben, die ein Array mit möglichen Dateipfaden zurückgibt. +Die Dateien, in denen Layoutvorlagen gesucht werden, können durch Überschreiben der Methode [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] geändert werden, die ein Array mit möglichen Dateinamen zurückgibt. Variablen in der Vorlage .[#toc-variables-in-the-template] @@ -104,7 +150,7 @@ Die `@property-read` Annotation ist für die IDE und die statische Analyse, sie Sie können sich auch den Luxus gönnen, in Vorlagen zu flüstern. Installieren Sie einfach das Latte-Plugin in PhpStorm und geben Sie den Klassennamen am Anfang der Vorlage an, siehe den Artikel "Latte: how to type system":https://blog.nette.org/de/latte-wie-benutzt-man-das-typensystem: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void Latte Version 3 bietet einen fortgeschritteneren Weg, indem es eine [Erweiterung |latte:creating-extension] für jedes Webprojekt erstellt. Hier ist ein grobes Beispiel für eine solche Klasse: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ Wir registrieren sie mit [configuration |configuration#Latte]: ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ Alternativ kann der Übersetzer auch über die [Konfiguration |configuration#Lat ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` Der Übersetzer kann dann z.B. als Filter `|translate` verwendet werden, wobei zusätzliche Parameter an die Methode `translate()` übergeben werden (siehe `foo, bar`): diff --git a/application/el/@home.texy b/application/el/@home.texy index 8020e59b5f..5d89df5402 100644 --- a/application/el/@home.texy +++ b/application/el/@home.texy @@ -2,35 +2,84 @@ ************** .[perex] -Το πακέτο `nette/application` αποτελεί τη βάση για τη δημιουργία διαδραστικών εφαρμογών ιστού. +Το Nette Application είναι ο πυρήνας του πλαισίου Nette που προσφέρει ισχυρά εργαλεία για τη δημιουργία σύγχρονων εφαρμογών ιστού. Προσφέρει πολυάριθμα εξαιρετικά χαρακτηριστικά που απλοποιούν σημαντικά την ανάπτυξη και βελτιώνουν την ασφάλεια και τη συντηρησιμότητα του κώδικα. -- [Πώς λειτουργούν οι εφαρμογές; |how-it-works] -- [Bootstrap |Bootstrap] -- [Παρουσιαστές |Presenters] -- [Πρότυπα |Templates] -- [Ενότητες |Modules] -- [Δρομολόγηση |Routing] -- [Δημιουργία συνδέσμων URL |creating-links] -- [Διαδραστικά στοιχεία |components] -- [AJAX & Snippets |ajax] -- [Πολλαπλασιαστής |multiplier] -- [Διαμόρφωση |Configuration] +Εγκατάσταση .[#toc-installation] +-------------------------------- -Εγκατάσταση ------------ - -Κατεβάστε και εγκαταστήστε το πακέτο χρησιμοποιώντας το [Composer |best-practices:composer]: +Κατεβάστε και εγκαταστήστε τη βιβλιοθήκη χρησιμοποιώντας [το Composer |best-practices:composer]: ```shell composer require nette/application ``` -| έκδοση | συμβατό με PHP + +Γιατί να επιλέξετε την εφαρμογή Nette; .[#toc-why-choose-nette-application] +--------------------------------------------------------------------------- + +Η Nette ήταν πάντα πρωτοπόρος στις τεχνολογίες ιστού. + +**Αμφίδρομος δρομολογητής:** Η Nette διαθέτει ένα προηγμένο σύστημα δρομολόγησης μοναδικό στην αμφίδρομη λειτουργία του - δεν μεταφράζει μόνο τις διευθύνσεις URL σε ενέργειες της εφαρμογής, αλλά μπορεί επίσης να παράγει διευθύνσεις URL αντίστροφα. Αυτό σημαίνει ότι: +- Μπορείτε να τροποποιήσετε τη δομή των διευθύνσεων URL ολόκληρης της εφαρμογής ανά πάσα στιγμή χωρίς να τροποποιήσετε τα αρχεία προτύπων +- Οι διευθύνσεις URL κανονικοποιούνται αυτόματα, βελτιώνοντας το SEO +- Η δρομολόγηση ορίζεται σε ένα μέρος, όχι διάσπαρτα σε σημειώσεις + +**Συστατικά και σήματα:** Το ενσωματωμένο σύστημα συστατικών εμπνευσμένο από τους Delphi και το React.js είναι μοναδικό μεταξύ των πλαισίων PHP: +- Επιτρέπει τη δημιουργία επαναχρησιμοποιήσιμων στοιχείων UI +- Υποστηρίζει ιεραρχική σύνθεση συστατικών +- Προσφέρει κομψό χειρισμό αιτήσεων AJAX με χρήση σημάτων +- Πλούσια βιβλιοθήκη έτοιμων στοιχείων στο [Componette](https://componette.org) + +**AJAX και Snippets:** Η Nette εισήγαγε έναν επαναστατικό τρόπο εργασίας με AJAX το 2009, πριν από λύσεις όπως το Hotwire για Ruby on Rails ή το Symfony UX Turbo: +- Τα Snippets επιτρέπουν την ενημέρωση μόνο τμημάτων της σελίδας χωρίς τη συγγραφή JavaScript +- Αυτόματη ενσωμάτωση με το σύστημα συστατικών +- Έξυπνη ακύρωση τμημάτων της σελίδας +- Ελάχιστη μεταφορά δεδομένων + +**Εξυπηρέτηση [με Latte |latte:] Templates:** Το πιο ασφαλές σύστημα template για PHP με προηγμένα χαρακτηριστικά: +- Αυτόματη προστασία XSS με διαφυγή με ευαισθησία περιβάλλοντος +- Επεκτάσιμο με προσαρμοσμένα φίλτρα, συναρτήσεις και ετικέτες +- Κληρονομικότητα προτύπων και αποσπάσματα για AJAX +- Εξαιρετική υποστήριξη της PHP 8.x με σύστημα τύπων + +**Έγχυση Εξαρτήσεων:** Η Nette χρησιμοποιεί πλήρως την έγχυση εξαρτήσεων: +- Αυτόματο πέρασμα εξαρτήσεων (autowiring) +- Ρύθμιση παραμέτρων με τη χρήση σαφούς μορφής NEON +- Υποστήριξη για εργοστάσια συστατικών + + +Κύρια οφέλη .[#toc-main-benefits] +--------------------------------- + +- **Ασφάλεια**: XSS, CSRF κ.λπ. +- **Παραγωγικότητα**: Λιγότερο γράψιμο, περισσότερες δυνατότητες χάρη στον έξυπνο σχεδιασμό +- **Αποσφαλμάτωση**: [Αποσφαλματωτής Tracy |tracy:] με πίνακα δρομολόγησης +- **Απόδοση**: Ευφυές σύστημα προσωρινής αποθήκευσης, χαλαρή φόρτωση των στοιχείων +- **Ευελιξία**: Εύκολη τροποποίηση της διεύθυνσης URL ακόμη και μετά την ολοκλήρωση της εφαρμογής +- **Συστατικά**: Μοναδικό σύστημα επαναχρησιμοποιήσιμων στοιχείων UI +- **Σύγχρονο**: Πλήρης υποστήριξη για PHP 8.4+ και σύστημα τύπων + + +Ξεκινώντας .[#toc-getting-started] +---------------------------------- + +1. [Κατανόηση των εφαρμογών |how-it-works] - Κατανόηση της βασικής αρχιτεκτονικής +2. [Παρουσιάστριες |presenters] - Εργασία με παρουσιάστριες και ενέργειες +3. [Πρότυπα |templates] - Δημιουργία προτύπων στο Latte +4. [Δρομολόγηση |routing] - Διαμόρφωση URL +5. [Διαδραστικά συστατικά |components] - Χρήση του συστήματος συστατικών + + +Συμβατότητα PHP .[#toc-php-compatibility] +----------------------------------------- + +| έκδοση | συμβατή με PHP |-----------|------------------- -| Εφαρμογή Nette 4.0 | PHP 8.0 - 8.2 -| Nette Application 3.1 | PHP 7.2 - 8.2 +| Nette Application 4.0 | PHP 8.1 - 8.4 +| Nette Application 3.2 | PHP 8.1 - 8.4 +| Nette Application 3.1 | PHP 7.2 - 8.3 | Nette Application 3.0 | PHP 7.1 - 8.0 | Nette Application 2.4 | PHP 5.6 - 8.0 -Ισχύει για τις τελευταίες εκδόσεις διορθώσεων. +Ισχύει για τις τελευταίες εκδόσεις διορθώσεων. \ No newline at end of file diff --git a/application/el/@left-menu.texy b/application/el/@left-menu.texy index b704f8e06d..6406189155 100644 --- a/application/el/@left-menu.texy +++ b/application/el/@left-menu.texy @@ -4,7 +4,7 @@ - [Bootstrap |Bootstrap] - [Παρουσιαστές |Presenters] - [Πρότυπα |Templates] -- [Ενότητες |Modules] +- [Δομή καταλόγου |directory-structure] - [Δρομολόγηση |Routing] - [Δημιουργία συνδέσμων URL |creating-links] - [Διαδραστικά στοιχεία |components] diff --git a/application/el/ajax.texy b/application/el/ajax.texy index 8ca0741a48..ab0e2f48f5 100644 --- a/application/el/ajax.texy +++ b/application/el/ajax.texy @@ -3,10 +3,10 @@ AJAX & Snippets
    -Οι σύγχρονες διαδικτυακές εφαρμογές τρέχουν σήμερα κατά το ήμισυ σε έναν διακομιστή και κατά το ήμισυ σε ένα πρόγραμμα περιήγησης. Το AJAX είναι ένας ζωτικής σημασίας ενωτικός παράγοντας. Τι υποστήριξη προσφέρει το Nette Framework; -- αποστολή τμημάτων προτύπου (τα λεγόμενα *snippets*) -- μεταβίβαση μεταβλητών μεταξύ PHP και JavaScript -- αποσφαλμάτωση εφαρμογών AJAX +Στην εποχή των σύγχρονων διαδικτυακών εφαρμογών, όπου η λειτουργικότητα συχνά εκτείνεται μεταξύ του διακομιστή και του προγράμματος περιήγησης, το AJAX είναι ένα απαραίτητο συνδετικό στοιχείο. Ποιες επιλογές προσφέρει το Nette Framework σε αυτόν τον τομέα; +- αποστολή τμημάτων του προτύπου, των λεγόμενων αποσπασμάτων +- διαβίβαση μεταβλητών μεταξύ PHP και JavaScript +- εργαλεία για την αποσφαλμάτωση αιτημάτων AJAX
    @@ -14,29 +14,32 @@ AJAX & Snippets Αίτηση AJAX .[#toc-ajax-request] ================================ -Ένα αίτημα AJAX δεν διαφέρει από ένα κλασικό αίτημα - ο παρουσιαστής καλείται με μια συγκεκριμένη προβολή και παραμέτρους. Εξαρτάται επίσης από τον παρουσιαστή πώς θα απαντήσει σε αυτό: μπορεί να χρησιμοποιήσει τη δική του ρουτίνα, η οποία επιστρέφει ένα τμήμα κώδικα HTML (απόσπασμα HTML), ένα έγγραφο XML, ένα αντικείμενο JSON ή κώδικα JavaScript. +Ένα αίτημα AJAX δεν διαφέρει ουσιαστικά από ένα κλασικό αίτημα HTTP. Ένας παρουσιαστής καλείται με συγκεκριμένες παραμέτρους. Από τον παρουσιαστή εξαρτάται πώς θα απαντήσει στο αίτημα - μπορεί να επιστρέψει δεδομένα σε μορφή JSON, να στείλει ένα τμήμα κώδικα HTML, ένα έγγραφο XML κ.λπ. -Από την πλευρά του διακομιστή, ένα αίτημα AJAX μπορεί να ανιχνευθεί χρησιμοποιώντας τη μέθοδο service που [ενθυλακώνει το αίτημα HTTP |http:request] `$httpRequest->isAjax()` (ανιχνεύει με βάση την επικεφαλίδα HTTP `X-Requested-With`). Στο εσωτερικό του παρουσιαστή, είναι διαθέσιμη μια συντόμευση με τη μορφή της μεθόδου `$this->isAjax()`. +Από την πλευρά του προγράμματος περιήγησης, ξεκινάμε ένα αίτημα AJAX χρησιμοποιώντας τη συνάρτηση `fetch()`: -Υπάρχει ένα προεπεξεργασμένο αντικείμενο που ονομάζεται `payload` και είναι αφιερωμένο στην αποστολή δεδομένων στο πρόγραμμα περιήγησης σε JSON. - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // επεξεργασία της απάντησης +}); ``` -Για πλήρη έλεγχο της εξόδου JSON χρησιμοποιήστε τη μέθοδο `sendJson` στον παρουσιαστή σας. Τερματίζει αμέσως τον presenter και θα κάνετε χωρίς πρότυπο: +Στην πλευρά του διακομιστή, ένα αίτημα AJAX αναγνωρίζεται από τη μέθοδο `$httpRequest->isAjax()` της υπηρεσίας που [ενθυλακώνει το αίτημα HTTP |http:request]. Χρησιμοποιεί την επικεφαλίδα HTTP `X-Requested-With`, οπότε είναι απαραίτητη η αποστολή της. Μέσα στον παρουσιαστή, μπορείτε να χρησιμοποιήσετε τη μέθοδο `$this->isAjax()`. + +Εάν θέλετε να στείλετε δεδομένα σε μορφή JSON, χρησιμοποιήστε τη μέθοδο [`sendJson()` |presenters#Sending a response] μέθοδο. Η μέθοδος τερματίζει επίσης τη δραστηριότητα του παρουσιαστή. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Εάν θέλουμε να στείλουμε HTML, μπορούμε είτε να ορίσουμε ένα ειδικό πρότυπο για αιτήσεις AJAX: +Αν σκοπεύετε να απαντήσετε με ένα ειδικό πρότυπο σχεδιασμένο για AJAX, μπορείτε να το κάνετε ως εξής: ```php public function handleClick($param): void @@ -44,27 +47,43 @@ public function handleClick($param): void if ($this->isAjax()) { $this->template->setFile('path/to/ajax.latte'); } - // ... + //... } ``` +Αποσπάσματα .[#toc-snippets] +============================ + +Το πιο ισχυρό εργαλείο που προσφέρει η Nette για τη σύνδεση του διακομιστή με τον πελάτη είναι τα snippets. Με αυτά μπορείτε να μετατρέψετε μια συνηθισμένη εφαρμογή σε AJAX με ελάχιστη προσπάθεια και λίγες γραμμές κώδικα. Το παράδειγμα Fifteen δείχνει πώς λειτουργούν όλα αυτά, και ο κώδικάς του μπορεί να βρεθεί στο [GitHub |https://github.com/nette-examples/fifteen]. + +Τα αποσπάσματα, ή αποκόμματα, σας επιτρέπουν να ενημερώνετε μόνο τμήματα της σελίδας, αντί να επαναφορτώνετε ολόκληρη τη σελίδα. Αυτό είναι πιο γρήγορο και αποτελεσματικό, και παρέχει επίσης μια πιο άνετη εμπειρία χρήσης. Τα αποσπάσματα μπορεί να σας θυμίζουν το Hotwire για το Ruby on Rails ή το Symfony UX Turbo. Είναι ενδιαφέρον ότι η Nette εισήγαγε τα snippets 14 χρόνια νωρίτερα. + +Πώς λειτουργούν τα snippets; Όταν φορτώνεται για πρώτη φορά η σελίδα (μια αίτηση χωρίς-AJAX), φορτώνεται ολόκληρη η σελίδα, συμπεριλαμβανομένων όλων των snippets. Όταν ο χρήστης αλληλεπιδρά με τη σελίδα (π.χ. κάνει κλικ σε ένα κουμπί, υποβάλλει μια φόρμα κ.λπ.), αντί να φορτωθεί ολόκληρη η σελίδα, γίνεται ένα αίτημα AJAX. Ο κώδικας στον παρουσιαστή εκτελεί την ενέργεια και αποφασίζει ποια αποσπάσματα χρειάζονται ενημέρωση. Η Nette αποδίδει αυτά τα αποσπάσματα και τα αποστέλλει με τη μορφή ενός πίνακα JSON. Ο κώδικας χειρισμού στο πρόγραμμα περιήγησης εισάγει στη συνέχεια τα ληφθέντα αποσπάσματα πίσω στη σελίδα. Επομένως, μεταφέρεται μόνο ο κώδικας των αλλαγμένων αποσπασμάτων, εξοικονομώντας εύρος ζώνης και επιταχύνοντας τη φόρτωση σε σύγκριση με τη μεταφορά ολόκληρου του περιεχομένου της σελίδας. + + Naja .[#toc-naja] -================= +----------------- -Η [βιβλιοθήκη Naja |https://naja.js.org] χρησιμοποιείται για το χειρισμό αιτημάτων AJAX στην πλευρά του προγράμματος περιήγησης. [Εγκαταστήστε |https://naja.js.org/#/guide/01-install-setup-naja] την ως πακέτο node.js (για χρήση με Webpack, Rollup, Vite, Parcel και άλλα): +Για το χειρισμό των αποσπασμάτων στην πλευρά του προγράμματος περιήγησης, χρησιμοποιείται η [βιβλιοθήκη Naja |https://naja.js.org]. [Εγκαταστήστε την |https://naja.js.org/#/guide/01-install-setup-naja] ως πακέτο node.js (για χρήση με εφαρμογές όπως Webpack, Rollup, Vite, Parcel και άλλες): ```shell npm install naja ``` -...ή να την εισαγάγετε απευθείας στο πρότυπο της σελίδας: +... ή να την εισαγάγετε απευθείας στο πρότυπο της σελίδας: ```html ``` -Για να δημιουργήσετε μια αίτηση AJAX από έναν κανονικό σύνδεσμο (σήμα) ή μια υποβολή φόρμας, απλά επισημάνετε τον σχετικό σύνδεσμο, τη φόρμα ή το κουμπί με την κλάση `ajax`: +Πρώτα πρέπει να [αρχικοποιήσετε |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] τη βιβλιοθήκη: + +```js +naja.initialize(); +``` + +Για να μετατρέψετε έναν συνηθισμένο σύνδεσμο (σήμα) ή την υποβολή φόρμας σε αίτηση AJAX, απλά σημειώστε τον αντίστοιχο σύνδεσμο, φόρμα ή κουμπί με την κλάση `ajax`: ```html Go @@ -74,64 +93,39 @@ npm install naja or +
    ``` -Snippets .[#toc-snippets] -========================= - -Υπάρχει ένα πολύ πιο ισχυρό εργαλείο ενσωματωμένης υποστήριξης AJAX - τα αποσπάσματα. Η χρήση τους καθιστά δυνατή τη μετατροπή μιας κανονικής εφαρμογής σε εφαρμογή AJAX χρησιμοποιώντας μόνο μερικές γραμμές κώδικα. Το πώς λειτουργούν όλα αυτά παρουσιάζεται στο παράδειγμα Fifteen του οποίου ο κώδικας είναι επίσης προσβάσιμος στο build ή στο [GitHub |https://github.com/nette-examples/fifteen]. - -Ο τρόπος που λειτουργούν τα snippets είναι ότι ολόκληρη η σελίδα μεταφέρεται κατά το αρχικό (δηλαδή μη-AJAX) αίτημα και στη συνέχεια με κάθε AJAX [υποερώτημα |components#signal] (αίτημα της ίδιας προβολής του ίδιου παρουσιαστή) μεταφέρεται μόνο ο κώδικας των αλλαγμένων τμημάτων στο αποθετήριο `payload` που αναφέρθηκε προηγουμένως. - -Τα Snippets μπορεί να σας θυμίζουν το Hotwire για το Ruby on Rails ή το Symfony UX Turbo, αλλά η Nette τα επινόησε δεκατέσσερα χρόνια νωρίτερα. - +Επανασχεδίαση αποσπασμάτων .[#toc-redrawing-snippets] +----------------------------------------------------- -Ακύρωση των Snippets .[#toc-invalidation-of-snippets] -===================================================== - -Κάθε απόγονος της κλάσης [Control |components] (που είναι και ένας Παρουσιαστής) είναι σε θέση να θυμάται αν υπήρξαν αλλαγές κατά τη διάρκεια μιας αίτησης που απαιτούν την εκ νέου εμφάνιση. Υπάρχει ένα ζευγάρι μεθόδων για το χειρισμό αυτό: `redrawControl()` και `isControlInvalid()`. Ένα παράδειγμα: +Κάθε αντικείμενο της κλάσης [Control |components] (συμπεριλαμβανομένου και του ίδιου του Presenter) διατηρεί αρχείο για το αν έχουν συμβεί αλλαγές που καθιστούν αναγκαία την επανασχεδίασή του. Για το σκοπό αυτό χρησιμοποιείται η μέθοδος `redrawControl()`. ```php public function handleLogin(string $user): void { - // Το αντικείμενο πρέπει να αναδημιουργηθεί εκ νέου μετά τη σύνδεση του χρήστη. + // μετά τη σύνδεση, είναι απαραίτητο να σχεδιάσετε εκ νέου το σχετικό τμήμα $this->redrawControl(); - // ... + //... } ``` -Η Nette ωστόσο προσφέρει μια ακόμη πιο λεπτή ανάλυση από ολόκληρα στοιχεία. Οι αναφερόμενες μέθοδοι δέχονται το όνομα ενός λεγόμενου "αποσπάσματος" ως προαιρετική παράμετρο. Ένα "απόσπασμα" είναι ουσιαστικά ένα στοιχείο στο πρότυπό σας που επισημαίνεται για το σκοπό αυτό με μια ετικέτα Latte, περισσότερα γι' αυτό αργότερα. Έτσι είναι δυνατόν να ζητήσετε από ένα στοιχείο να ξανασχεδιάσει μόνο *μέρη* του προτύπου του. Εάν ακυρωθεί ολόκληρο το συστατικό, τότε όλα τα αποσπάσματά του αναδημιουργούνται εκ νέου. Ένα συστατικό είναι "άκυρο" επίσης εάν οποιοδήποτε από τα υποσυστήματά του είναι άκυρο. - -```php -$this->isControlInvalid(); // -> false -$this->redrawControl('header'); // ακυρώνει το απόσπασμα με το όνομα 'header' -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, τουλάχιστον ένα απόσπασμα είναι άκυρο +Η Nette επιτρέπει επίσης έναν πιο λεπτομερή έλεγχο του τι χρειάζεται επανασχεδίαση. Η προαναφερθείσα μέθοδος μπορεί να λάβει το όνομα του αποσπάσματος ως όρισμα. Έτσι, είναι δυνατή η ακύρωση (που σημαίνει: εξαναγκασμός σε επανασχεδίαση) σε επίπεδο τμήματος προτύπου. Εάν ακυρωθεί ολόκληρο το συστατικό, κάθε απόσπασμα αυτού επανασχεδιάζεται επίσης: -$this->redrawControl(); // ακυρώνει ολόκληρο το συστατικό, κάθε απόσπασμα -$this->isControlInvalid('footer'); // -> true +```php +// ακυρώνει το απόσπασμα 'header' +$this->redrawControl('header'); ``` -Ένα συστατικό που λαμβάνει σήμα επισημαίνεται αυτόματα για επανασχεδίαση. - -Χάρη στην επανασχεδίαση αποσπασμάτων γνωρίζουμε επακριβώς ποια τμήματα ποιων στοιχείων πρέπει να επανασχεδιαστούν. - - -Ετικέτα `{snippet} … {/snippet}` .{toc: Tag snippet} -==================================================== - -Η απόδοση της σελίδας εξελίσσεται πολύ παρόμοια με μια κανονική αίτηση: φορτώνονται τα ίδια πρότυπα κ.λπ. Το ζωτικής σημασίας μέρος είναι, ωστόσο, να παραλείπονται τα μέρη που δεν πρέπει να φτάσουν στην έξοδο- τα υπόλοιπα μέρη πρέπει να συσχετίζονται με ένα αναγνωριστικό και να αποστέλλονται στο χρήστη σε κατανοητή μορφή για έναν χειριστή JavaScript. - -Σύνταξη .[#toc-syntax] ----------------------- +Αποσπάσματα σε Latte .[#toc-snippets-in-latte] +---------------------------------------------- -Εάν υπάρχει ένα στοιχείο ελέγχου ή ένα απόσπασμα στο πρότυπο, πρέπει να το τυλίξουμε χρησιμοποιώντας την ετικέτα `{snippet} ... {/snippet}` pair - θα διασφαλίσει ότι το αποδιδόμενο απόσπασμα θα "αποκοπεί" και θα σταλεί στο πρόγραμμα περιήγησης. Θα το περικλείσει επίσης σε ένα βοηθητικό `
    ` tag (είναι δυνατόν να χρησιμοποιηθεί ένα διαφορετικό). Στο ακόλουθο παράδειγμα ορίζεται ένα απόσπασμα με το όνομα `header`. Μπορεί κάλλιστα να αντιπροσωπεύει το πρότυπο ενός στοιχείου: +Η χρήση αποσπασμάτων στο Latte είναι εξαιρετικά εύκολη. Για να ορίσετε ένα μέρος του προτύπου ως snippet, απλά τυλίξτε το σε ετικέτες `{snippet}` και `{/snippet}`: ```latte {snippet header} @@ -139,7 +133,9 @@ $this->isControlInvalid('footer'); // -> true {/snippet} ``` -Ένα απόσπασμα άλλου τύπου από το `
    ` ή ένα απόσπασμα με πρόσθετα χαρακτηριστικά HTML επιτυγχάνεται με τη χρήση της παραλλαγής χαρακτηριστικών: +Το απόσπασμα δημιουργεί ένα στοιχείο `
    ` στη σελίδα HTML με ένα ειδικά δημιουργημένο `id`. Κατά την επανασχεδίαση ενός αποσπάσματος, το περιεχόμενο αυτού του στοιχείου ενημερώνεται. Επομένως, κατά την αρχική απόδοση της σελίδας, όλα τα snippets πρέπει επίσης να αποδοθούν, ακόμη και αν μπορεί αρχικά να είναι κενά. + +Μπορείτε επίσης να δημιουργήσετε ένα απόσπασμα με ένα στοιχείο διαφορετικό από το `
    ` χρησιμοποιώντας ένα χαρακτηριστικό n:attribute: ```latte
    @@ -148,138 +144,106 @@ $this->isControlInvalid('footer'); // -> true ``` -Δυναμικά αποσπάσματα .[#toc-dynamic-snippets] -============================================= +Περιοχές αποσπασμάτων .[#toc-snippet-areas] +------------------------------------------- -Στο Nette μπορείτε επίσης να ορίσετε αποσπάσματα με δυναμικό όνομα βάσει μιας παραμέτρου εκτέλεσης. Αυτό είναι πιο κατάλληλο για διάφορες λίστες όπου πρέπει να αλλάξουμε μόνο μια γραμμή αλλά δεν θέλουμε να μεταφέρουμε ολόκληρη τη λίστα μαζί με αυτήν. Ένα τέτοιο παράδειγμα θα ήταν το εξής: +Τα ονόματα αποσπασμάτων μπορούν επίσης να είναι εκφράσεις: ```latte -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Υπάρχει ένα στατικό απόσπασμα που ονομάζεται `itemsContainer`, το οποίο περιέχει διάφορα δυναμικά αποσπάσματα: `item-0`, `item-1` κ.ο.κ. + `item-0`, `item-1`, κ.λπ. Αν ακυρώναμε άμεσα ένα δυναμικό απόσπασμα (π.χ. `item-1`), τίποτα δεν θα ξανασχεδιαζόταν. Ο λόγος είναι ότι τα αποσπάσματα λειτουργούν ως αληθινά αποσπάσματα και μόνο τα ίδια αποδίδονται άμεσα. Ωστόσο, στο πρότυπο, δεν υπάρχει τεχνικά ένα απόσπασμα με το όνομα `item-1`. Εμφανίζεται μόνο όταν εκτελείται ο περιβάλλων κώδικας του αποσπάσματος, στην προκειμένη περίπτωση, ο βρόχος foreach. Ως εκ τούτου, θα επισημάνουμε το τμήμα του προτύπου που πρέπει να εκτελεστεί με την ετικέτα `{snippetArea}`: -Δεν μπορείτε να ξανασχεδιάσετε άμεσα ένα δυναμικό απόσπασμα (η επανασχεδίαση του `item-1` δεν έχει κανένα αποτέλεσμα), πρέπει να ξανασχεδιάσετε το γονικό του απόσπασμα (σε αυτό το παράδειγμα `itemsContainer`). Αυτό προκαλεί την εκτέλεση του κώδικα του γονικού αποσπάσματος, αλλά στη συνέχεια αποστέλλονται στο πρόγραμμα περιήγησης μόνο τα επιμέρους αποσπάσματά του. Αν θέλετε να στείλετε μόνο ένα από τα υπο-στοιχεία, πρέπει να τροποποιήσετε την είσοδο για το γονικό απόσπασμα ώστε να μην παράγει τα άλλα υπο-στοιχεία. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -Στο παραπάνω παράδειγμα πρέπει να βεβαιωθείτε ότι για μια αίτηση AJAX θα προστεθεί μόνο ένα στοιχείο στον πίνακα `$list`, επομένως ο βρόχος `foreach` θα εκτυπώσει μόνο ένα δυναμικό απόσπασμα. +Και θα ξανασχεδιάσουμε τόσο το μεμονωμένο απόσπασμα όσο και ολόκληρη την υπερκείμενη περιοχή: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * This method returns data for the list. - * Usually this would just request the data from a model. - * For the purpose of this example, the data is hard-coded. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +Είναι επίσης σημαντικό να διασφαλίσουμε ότι ο πίνακας `$items` περιέχει μόνο τα στοιχεία που πρέπει να επανασχεδιαστούν. -Αποσπάσματα σε συμπεριλαμβανόμενο πρότυπο .[#toc-snippets-in-an-included-template] -================================================================================== - -Μπορεί να συμβεί το απόσπασμα να βρίσκεται σε ένα πρότυπο το οποίο συμπεριλαμβάνεται από ένα διαφορετικό πρότυπο. Σε αυτή την περίπτωση πρέπει να τυλίξουμε τον κώδικα συμπερίληψης στο δεύτερο πρότυπο με την ετικέτα `snippetArea`, και στη συνέχεια να ξανασχεδιάσουμε τόσο το snippetArea όσο και το πραγματικό απόσπασμα. - -Η ετικέτα `snippetArea` διασφαλίζει ότι ο κώδικας στο εσωτερικό της εκτελείται, αλλά μόνο το πραγματικό απόσπασμα στο συμπεριλαμβανόμενο πρότυπο αποστέλλεται στο πρόγραμμα περιήγησης. +Κατά την εισαγωγή ενός άλλου προτύπου στο κύριο με τη χρήση της ετικέτας `{include}`, το οποίο έχει αποσπάσματα, είναι απαραίτητο να τυλίξετε και πάλι το συμπεριλαμβανόμενο πρότυπο σε ένα `snippetArea` και να ακυρώσετε τόσο το απόσπασμα όσο και την περιοχή μαζί: ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* included.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -Μπορείτε επίσης να το συνδυάσετε με δυναμικά αποσπάσματα. +Αποσπάσματα σε στοιχεία .[#toc-snippets-in-components] +------------------------------------------------------ -Προσθήκη και διαγραφή .[#toc-adding-and-deleting] -================================================= - -Εάν προσθέσετε ένα νέο στοιχείο στη λίστα και ακυρώσετε το `itemsContainer`, η αίτηση AJAX επιστρέφει αποσπάσματα που περιλαμβάνουν το νέο στοιχείο, αλλά ο χειριστής javascript δεν θα είναι σε θέση να το αποδώσει. Αυτό συμβαίνει επειδή δεν υπάρχει κανένα στοιχείο HTML με το νεοδημιουργηθέν ID. - -Σε αυτή την περίπτωση, ο απλούστερος τρόπος είναι να τυλίξετε ολόκληρη τη λίστα σε ένα ακόμη απόσπασμα και να τα ακυρώσετε όλα: +Μπορείτε να δημιουργήσετε αποσπάσματα μέσα σε [στοιχεία |components] και η Nette θα τα ανασχεδιάσει αυτόματα. Ωστόσο, υπάρχει ένας συγκεκριμένος περιορισμός: για να ξανασχεδιάσει αποσπάσματα, καλεί τη μέθοδο `render()` χωρίς καμία παράμετρο. Έτσι, η μετάδοση παραμέτρων στο πρότυπο δεν θα λειτουργήσει: ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Αποστολή δεδομένων χρήστη .[#toc-sending-user-data] +--------------------------------------------------- + +Μαζί με τα αποσπάσματα, μπορείτε να στείλετε οποιαδήποτε πρόσθετα δεδομένα στον πελάτη. Απλά γράψτε τα στο αντικείμενο `payload`: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + //... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -Το ίδιο ισχύει και για τη διαγραφή ενός στοιχείου. Θα ήταν δυνατό να στείλετε κενό snippet, αλλά συνήθως οι λίστες μπορούν να είναι σελιδοποιημένες και θα ήταν περίπλοκο να υλοποιήσετε τη διαγραφή ενός στοιχείου και τη φόρτωση ενός άλλου (το οποίο βρισκόταν σε διαφορετική σελίδα της σελιδοποιημένης λίστας). - -Αποστολή παραμέτρων στο συστατικό .[#toc-sending-parameters-to-component] -========================================================================= +Παράμετροι αποστολής .[#toc-sending-parameters] +=============================================== Όταν στέλνουμε παραμέτρους στο στοιχείο μέσω αίτησης AJAX, είτε πρόκειται για παραμέτρους σήματος είτε για μόνιμες παραμέτρους, πρέπει να παρέχουμε το συνολικό τους όνομα, το οποίο περιέχει επίσης το όνομα του στοιχείου. Το πλήρες όνομα της παραμέτρου επιστρέφει η μέθοδος `getParameterId()`. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -Και χειρίζεται τη μέθοδο με s αντίστοιχες παραμέτρους στο συστατικό. +Μια μέθοδος χειρισμού με τις αντίστοιχες παραμέτρους στο συστατικό: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/el/bootstrap.texy b/application/el/bootstrap.texy index 71f6028048..3f5515fbd3 100644 --- a/application/el/bootstrap.texy +++ b/application/el/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + 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 { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // Η Nette είναι έξυπνη και η λειτουργία ανάπτυξης ενεργοποιείται αυτόματα, + // ή μπορείτε να την ενεργοποιήσετε για μια συγκεκριμένη διεύθυνση IP ξεσχολιάζοντας την ακόλουθη γραμμή: + // $this->configurator->setDebugMode('secret@23.75.345.200'), + + // Ενεργοποιεί το Tracy: το απόλυτο εργαλείο αποσφαλμάτωσης "ελβετικό μαχαίρι του στρατού". + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: αυτόματη φόρτωση όλων των κλάσεων στον δεδομένο κατάλογο + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Φόρτωση αρχείων διαμόρφωσης + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php .[#toc-index-php] =========================== -Στην περίπτωση των διαδικτυακών εφαρμογών, το αρχικό αρχείο είναι το `index.php`, το οποίο βρίσκεται στον δημόσιο κατάλογο `www/`. Επιτρέπει στην κλάση `Bootstrap` να αρχικοποιήσει το περιβάλλον και να επιστρέψει το `$configurator` που δημιουργεί το DI container. Στη συνέχεια αποκτά την υπηρεσία `Application`, η οποία εκτελεί την εφαρμογή ιστού: +Στην περίπτωση των διαδικτυακών εφαρμογών, το πρωτεύον αρχείο είναι το `index.php`, το οποίο βρίσκεται στον [δημόσιο κατάλογο |directory-structure#public-directory-www] `www/`. Αυτό θα έχει την κλάση Bootstrap να αρχικοποιεί το περιβάλλον και να παράγει ένα δοχείο DI. Στη συνέχεια, παίρνει από αυτό την υπηρεσία `Application`, η οποία εκκινεί την εφαρμογή ιστού: ```php -// αρχικοποίηση του περιβάλλοντος + λήψη του αντικειμένου Configurator -$configurator = App\Bootstrap::boot(); -// Δημιουργία ενός δοχείου DI -$container = $configurator->createContainer(); +$bootstrap = new App\Bootstrap; +// Αρχικοποίηση του περιβάλλοντος + δημιουργία ενός δοχείου DI +$container = $bootstrap->bootWebApplication(); // Το δοχείο DI δημιουργεί ένα αντικείμενο Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -// έναρξη της εφαρμογής Nette +// Εκκίνηση της εφαρμογής Nette και χειρισμός της εισερχόμενης αίτησης $application->run(); ``` @@ -59,26 +84,42 @@ $application->run(); Λειτουργία ανάπτυξης έναντι λειτουργίας παραγωγής .[#toc-development-vs-production-mode] ======================================================================================== -Η Nette διακρίνει μεταξύ δύο βασικών τρόπων εκτέλεσης μιας αίτησης: ανάπτυξη και παραγωγή. Η λειτουργία ανάπτυξης επικεντρώνεται στη μέγιστη άνεση του προγραμματιστή, εμφανίζεται το Tracy, η προσωρινή μνήμη ενημερώνεται αυτόματα όταν αλλάζουν τα πρότυπα ή η διαμόρφωση του DI container, κ.λπ. Η λειτουργία παραγωγής επικεντρώνεται στην απόδοση, το Tracy καταγράφει μόνο τα σφάλματα και δεν ελέγχονται οι αλλαγές των προτύπων και άλλων αρχείων. +Η Nette συμπεριφέρεται διαφορετικά ανάλογα με το αν εκτελείται σε διακομιστή ανάπτυξης ή παραγωγής: + +🛠️ Λειτουργία ανάπτυξης: + - Εμφανίζει τη γραμμή εντοπισμού σφαλμάτων του Tracy με χρήσιμες πληροφορίες (π.χ. ερωτήματα SQL, χρόνος εκτέλεσης, χρήση μνήμης). + - Εμφανίζει μια λεπτομερή σελίδα σφαλμάτων με ίχνη κλήσεων συναρτήσεων και περιεχόμενα μεταβλητών όταν εμφανίζεται σφάλμα. + - Ανανεώνει αυτόματα την προσωρινή μνήμη όταν τροποποιούνται πρότυπα Latte, αρχεία ρυθμίσεων κ.λπ. + + +🚀 Λειτουργία παραγωγής: + - Δεν εμφανίζει καμία πληροφορία εντοπισμού σφαλμάτων- όλα τα σφάλματα καταγράφονται. + - Εμφανίζει μια σελίδα `ErrorPresenter` ή μια γενική σελίδα "Σφάλμα διακομιστή" όταν προκύψει σφάλμα. + - Η προσωρινή μνήμη δεν ανανεώνεται ποτέ αυτόματα! + - Βελτιστοποιημένη για ταχύτητα και ασφάλεια. -Η επιλογή της λειτουργίας γίνεται με αυτόματη ανίχνευση, οπότε συνήθως δεν χρειάζεται να ρυθμίσετε ή να αλλάξετε κάτι χειροκίνητα. Η κατάσταση λειτουργίας είναι development εάν η εφαρμογή εκτελείται στο localhost (δηλαδή στη διεύθυνση IP `127.0.0.1` ή `::1`) και δεν υπάρχει proxy (δηλαδή η επικεφαλίδα HTTP του). Διαφορετικά, εκτελείται σε κατάσταση παραγωγής. + +Η λειτουργία καθορίζεται αυτόματα, οπότε στις περισσότερες περιπτώσεις δεν χρειάζεται να τη ρυθμίσετε ή να την αλλάξετε χειροκίνητα: + +- Λειτουργία ανάπτυξης: `127.0.0.1` ή `::1`), εκτός εάν χρησιμοποιείται ένας διακομιστής μεσολάβησης (δηλ. με βάση τις επικεφαλίδες HTTP). +- Λειτουργία παραγωγής: Ενεργός παντού αλλού. Αν θέλετε να ενεργοποιήσετε τη λειτουργία ανάπτυξης σε άλλες περιπτώσεις, για παράδειγμα, για προγραμματιστές που έχουν πρόσβαση από μια συγκεκριμένη διεύθυνση IP, μπορείτε να χρησιμοποιήσετε τη διεύθυνση `setDebugMode()`: ```php -$configurator->setDebugMode('23.75.345.200'); // μία ή περισσότερες διευθύνσεις IP +$this->configurator->setDebugMode('23.75.345.200'); // μία ή περισσότερες διευθύνσεις IP ``` Συνιστούμε οπωσδήποτε τον συνδυασμό μιας διεύθυνσης IP με ένα cookie. Θα αποθηκεύσουμε ένα μυστικό token στο cookie `nette-debug`, π.χ. `secret1234`, και η λειτουργία ανάπτυξης θα ενεργοποιηθεί για τους προγραμματιστές με αυτόν τον συνδυασμό IP και cookie. ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` Μπορούμε επίσης να απενεργοποιήσουμε εντελώς τη λειτουργία προγραμματιστή, ακόμη και για το localhost: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` Σημειώστε ότι η τιμή `true` ενεργοποιεί τη λειτουργία προγραμματιστή με σκληρό τρόπο, κάτι που δεν πρέπει ποτέ να συμβαίνει σε έναν διακομιστή παραγωγής. @@ -90,7 +131,7 @@ $configurator->setDebugMode(false); Για εύκολη αποσφαλμάτωση, θα ενεργοποιήσουμε το σπουδαίο εργαλείο [Tracy |tracy:]. Στη λειτουργία προγραμματιστή απεικονίζει τα σφάλματα και στη λειτουργία παραγωγής καταγράφει τα σφάλματα στον καθορισμένο κατάλογο: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ $configurator->enableTracy($appDir . '/log'); Η Nette χρησιμοποιεί την κρυφή μνήμη για το DI container, το RobotLoader, τα πρότυπα κ.λπ. Ως εκ τούτου, είναι απαραίτητο να ορίσετε τη διαδρομή προς τον κατάλογο όπου θα αποθηκεύεται η προσωρινή μνήμη: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` Σε Linux ή macOS, ορίστε τα [δικαιώματα εγγραφής |nette:troubleshooting#Setting directory permissions] για τους καταλόγους `log/` και `temp/`. @@ -112,7 +153,7 @@ RobotLoader .[#toc-robotloader] Συνήθως, θα θέλουμε να φορτώνουμε αυτόματα τις κλάσεις χρησιμοποιώντας [τον RobotLoader |robot-loader:], οπότε πρέπει να τον εκκινήσουμε και να τον αφήσουμε να φορτώσει κλάσεις από τον κατάλογο όπου βρίσκεται το `Bootstrap.php` (δηλαδή το `__DIR__`) και όλους τους υποκαταλόγους του: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ $configurator->createRobotLoader() Το Configurator σας επιτρέπει να καθορίσετε μια ζώνη ώρας για την εφαρμογή σας. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ $configurator->setTimeZone('Europe/Prague'); Τα αρχεία διαμόρφωσης φορτώνονται με τη χρήση του `addConfig()`: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` Η μέθοδος `addConfig()` μπορεί να κληθεί πολλές φορές για την προσθήκη πολλών αρχείων. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ if (PHP_SAPI === 'cli') { Οι παράμετροι που χρησιμοποιούνται σε αρχεία ρυθμίσεων μπορούν να οριστούν [στην ενότητα `parameters` |dependency-injection:configuration#parameters] και επίσης να μεταβιβαστούν (ή να αντικατασταθούν) από τη μέθοδο `addStaticParameters()` (έχει το ψευδώνυμο `addParameters()`). Είναι σημαντικό ότι διαφορετικές τιμές παραμέτρων προκαλούν τη δημιουργία πρόσθετων δοχείων DI, δηλαδή πρόσθετων κλάσεων. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ $configurator->addStaticParameters([ Μπορούμε επίσης να προσθέσουμε δυναμικές παραμέτρους στο δοχείο, οι διαφορετικές τιμές τους, σε αντίθεση με τις στατικές παραμέτρους, δεν θα προκαλέσουν τη δημιουργία νέων δοχείων DI. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ Οι μεταβλητές περιβάλλοντος θα μπορούσαν εύκολα να γίνουν διαθέσιμες με τη χρήση δυναμικών παραμέτρων. Μπορούμε να έχουμε πρόσβαση σε αυτές μέσω της διεύθυνσης `%env.variable%` στα αρχεία ρυθμίσεων. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ $configurator->addDynamicParameters([ - `%wwwDir%` είναι η απόλυτη διαδρομή προς τον κατάλογο που περιέχει το αρχείο καταχώρησης `index.php` - `%tempDir%` είναι η απόλυτη διαδρομή προς τον κατάλογο για τα προσωρινά αρχεία - `%vendorDir%` είναι η απόλυτη διαδρομή προς τον κατάλογο όπου ο Composer εγκαθιστά τις βιβλιοθήκες +- `%rootDir%` είναι η απόλυτη διαδρομή προς τον ριζικό κατάλογο του έργου - Το `%debugMode%` δηλώνει αν η εφαρμογή βρίσκεται σε κατάσταση αποσφαλμάτωσης. - Το `%consoleMode%` δηλώνει αν η αίτηση υποβλήθηκε μέσω της γραμμής εντολών. @@ -225,7 +268,7 @@ services: Δημιουργούμε μια νέα περίπτωση και την εισάγουμε στο bootstrap: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Διαφορετικά περιβάλλοντα .[#toc-different-environments] ======================================================= -Μπορείτε να προσαρμόσετε την τάξη `Bootstrap` ανάλογα με τις ανάγκες σας. Μπορείτε να προσθέσετε παραμέτρους στη μέθοδο `boot()` για να διαφοροποιήσετε τα έργα ιστού ή να προσθέσετε άλλες μεθόδους, όπως η `bootForTests()`, η οποία αρχικοποιεί το περιβάλλον για δοκιμές μονάδας, η `bootForCli()` για σενάρια που καλούνται από τη γραμμή εντολών κ.ο.κ. +Μη διστάσετε να προσαρμόσετε την τάξη `Bootstrap` σύμφωνα με τις ανάγκες σας. Μπορείτε να προσθέσετε παραμέτρους στη μέθοδο `bootWebApplication()` για να διαφοροποιήσετε τα διάφορα web projects. Εναλλακτικά, μπορείτε να προσθέσετε άλλες μεθόδους, όπως `bootTestEnvironment()` για την αρχικοποίηση του περιβάλλοντος για δοκιμές μονάδας, `bootConsoleApplication()` για σενάρια που καλούνται από τη γραμμή εντολών κ.ο.κ. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container { - $configurator = self::boot(); Tester\Environment::setup(); // Αρχικοποίηση Nette Tester - return $configurator; + $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 index ae5fc64c2f..06fafcf4d4 100644 --- a/application/el/components.texy +++ b/application/el/components.texy @@ -230,6 +230,28 @@ $this->redirect(/* ... */); // και ανακατεύθυνση ``` +Επανακατεύθυνση μετά από ένα σήμα .[#toc-redirection-after-a-signal] +==================================================================== + +Μετά την επεξεργασία ενός σήματος συνιστωσών, ακολουθεί συχνά ανακατεύθυνση. Αυτή η κατάσταση είναι παρόμοια με τις φόρμες - μετά την υποβολή μιας φόρμας, κάνουμε επίσης ανακατεύθυνση για να αποτρέψουμε την εκ νέου υποβολή δεδομένων όταν η σελίδα ανανεώνεται στο πρόγραμμα περιήγησης. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Δεδομένου ότι ένα συστατικό είναι ένα επαναχρησιμοποιήσιμο στοιχείο και συνήθως δεν πρέπει να έχει άμεση εξάρτηση από συγκεκριμένους παρουσιαστές, οι μέθοδοι `redirect()` και `link()` ερμηνεύουν αυτόματα την παράμετρο ως σήμα συστατικού: + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +Εάν χρειάζεται να ανακατευθύνετε σε διαφορετικό παρουσιαστή ή ενέργεια, μπορείτε να το κάνετε μέσω του παρουσιαστή: + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Μόνιμες παράμετροι .[#toc-persistent-parameters] ================================================ @@ -347,7 +369,7 @@ services: Τέλος, θα χρησιμοποιήσουμε αυτό το εργοστάσιο στον παρουσιαστή μας: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ interface PollControlFactory Τα συστατικά σε μια εφαρμογή Nette είναι τα επαναχρησιμοποιήσιμα μέρη μιας διαδικτυακής εφαρμογής που ενσωματώνουμε σε σελίδες, τα οποία αποτελούν το αντικείμενο αυτού του κεφαλαίου. Ποιες ακριβώς είναι οι δυνατότητες ενός τέτοιου συστατικού; 1) είναι δυνατό να αποδοθεί σε ένα πρότυπο -2) γνωρίζει ποιο μέρος του εαυτού του να αποδώσει κατά τη διάρκεια μιας [αίτησης AJAX |ajax#invalidation] (αποσπάσματα) +2) γνωρίζει [ποιο μέρος του εαυτού του |ajax#snippets] να αποδώσει κατά τη διάρκεια μιας αίτησης AJAX (αποσπάσματα) 3) έχει τη δυνατότητα να αποθηκεύει την κατάστασή του σε μια διεύθυνση URL (μόνιμες παράμετροι) 4) έχει τη δυνατότητα να ανταποκρίνεται σε ενέργειες του χρήστη (σήματα) 5) δημιουργεί μια ιεραρχική δομή (όπου η ρίζα είναι ο παρουσιαστής) @@ -430,7 +452,7 @@ class PaginatingControl extends Control } ``` -Η αντίθετη διαδικασία, δηλαδή η συλλογή τιμών από persistent properites, αντιμετωπίζεται από τη μέθοδο `saveState()`. +Η αντίθετη διαδικασία, δηλαδή η συλλογή τιμών από persistent properties, αντιμετωπίζεται από τη μέθοδο `saveState()`. Σήματα σε βάθος .[#toc-signals-in-depth] diff --git a/application/el/configuration.texy b/application/el/configuration.texy index 01b036704b..5ac500455c 100644 --- a/application/el/configuration.texy +++ b/application/el/configuration.texy @@ -13,11 +13,15 @@ application: # δείχνει τον πίνακα "Nette Application" στο Tracy BlueScreen? debugger: ... # (bool) προεπιλογή true - # θα καλείται ο παρουσιαστής σφαλμάτων σε περίπτωση σφάλματος; - catchExceptions: ... # (bool) προεπιλεγμένη τιμή true σε κατάσταση παραγωγής + # θα κληθεί ο παρουσιαστής σφαλμάτων στο σφάλμα; + # έχει αποτέλεσμα μόνο σε λειτουργία προγραμματιστή + catchExceptions: ... # (bool) προεπιλογή true # όνομα του error-presenter - errorPresenter: Error # (string) προεπιλογή 'Nette:Error' + errorPresenter: Error # (string|array) προεπιλογή 'Nette:Error' + + # ορίζει ψευδώνυμα για παρουσιαστές και εκδηλώσεις + aliases: ... # ορίζει τους κανόνες για την επίλυση του ονόματος του παρουσιαστή σε μια κλάση mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) προεπιλογή σε false ``` -Επειδή οι παρουσιαστές σφαλμάτων δεν καλούνται εξ ορισμού σε κατάσταση ανάπτυξης και τα σφάλματα εμφανίζονται από το Tracy, η αλλαγή της τιμής `catchExceptions` σε `true` βοηθάει στην επαλήθευση της σωστής λειτουργίας των παρουσιαστών σφαλμάτων κατά την ανάπτυξη. +Από την έκδοση 3.2 του `nette/application` είναι δυνατό να ορίσετε ένα ζεύγος παρουσιαστών σφαλμάτων: + +```neon +application: + errorPresenter: + 4xx: Error4xx # για την εξαίρεση Nette\Application\BadRequestException + 5xx: Error5xx # για άλλες εξαιρέσεις +``` Η επιλογή `silentLinks` καθορίζει τον τρόπο με τον οποίο η Nette συμπεριφέρεται στη λειτουργία ανάπτυξης όταν η δημιουργία συνδέσμων αποτυγχάνει (για παράδειγμα, επειδή δεν υπάρχει παρουσιαστής κ.λπ.). Η προεπιλεγμένη τιμή `false` σημαίνει ότι η Nette ενεργοποιεί το `E_USER_WARNING`. Η ρύθμιση σε `true` καταστέλλει αυτό το μήνυμα σφάλματος. Σε περιβάλλον παραγωγής, το `E_USER_WARNING` ενεργοποιείται πάντα. Μπορούμε επίσης να επηρεάσουμε αυτή τη συμπεριφορά θέτοντας τη μεταβλητή του παρουσιαστή [$invalidLinkMode |creating-links#Invalid Links]. -Η [αντιστοίχιση ορίζει τους κανόνες |modules#mapping] με τους οποίους το όνομα της κλάσης προκύπτει από το όνομα του παρουσιαστή. +Τα [ψευδώνυμα απλοποιούν την αναφορά σε |creating-links#aliases] συχνά χρησιμοποιούμενους παρουσιαστές. + +Η [αντιστοίχιση ορίζει τους κανόνες |directory-structure#Presenter Mapping] με τους οποίους το όνομα της κλάσης προκύπτει από το όνομα του παρουσιαστή. Αυτόματη εγγραφή παρουσιαστών .[#toc-automatic-registration-of-presenters] @@ -82,6 +95,9 @@ latte: # ενεργοποιεί τον [έλεγχο του παραγόμενου κώδικα |latte:develop#Checking Generated Code] phpLinter: ... # (string) η προεπιλογή είναι null + # ορίζει την τοπική γλώσσα + locale: cs_CZ # (string) η προεπιλογή είναι null + # κλάση του $this->template templateClass: App\MyTemplateClass # προεπιλογή σε Nette\Bridges\ApplicationLatte\DefaultTemplate ``` @@ -91,7 +107,7 @@ latte: ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` /--comment diff --git a/application/el/creating-links.texy b/application/el/creating-links.texy index e36d982225..19ec14635d 100644 --- a/application/el/creating-links.texy +++ b/application/el/creating-links.texy @@ -38,7 +38,7 @@ detail ``` -Εάν η μέθοδος `ProductPresenter::renderShow()` δεν έχει στην υπογραφή της την `$lang`, μπορεί να διαβάσει την τιμή της παραμέτρου χρησιμοποιώντας την `$lang = $this->getParameter('lang')`. +Εάν η μέθοδος `ProductPresenter::renderShow()` δεν έχει στην υπογραφή της την `$lang`, μπορεί να ανακτήσει την τιμή της παραμέτρου χρησιμοποιώντας την `$lang = $this->getParameter('lang')` ή από την [ιδιότητα |presenters#Request Parameters]. Εάν οι παράμετροι είναι αποθηκευμένες σε πίνακα, μπορούν να επεκταθούν με τον τελεστή `...` (ή `(expand)` στο Latte 2.x): @@ -103,7 +103,7 @@ $url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); home ``` -Οι σύνδεσμοι μπορούν επίσης να παραπέμπουν σε άλλες [ενότητες |modules]. Εδώ, οι σύνδεσμοι διακρίνονται σε σχετικούς με τις υποενότητες ή απόλυτους. Η αρχή είναι ανάλογη με τις διαδρομές δίσκου, μόνο που αντί για κάθετους υπάρχουν άνω και κάτω τελεία. Ας υποθέσουμε ότι ο πραγματικός παρουσιαστής είναι μέρος της ενότητας `Front`, τότε θα γράψουμε: +Οι σύνδεσμοι μπορούν επίσης να παραπέμπουν σε άλλες [ενότητες |directory-structure#Presenters and Templates]. Εδώ, οι σύνδεσμοι διακρίνονται σε σχετικούς με τις υποενότητες ή απόλυτους. Η αρχή είναι ανάλογη με τις διαδρομές δίσκου, μόνο που αντί για κάθετους υπάρχουν άνω και κάτω τελεία. Ας υποθέσουμε ότι ο πραγματικός παρουσιαστής είναι μέρος της ενότητας `Front`, τότε θα γράψουμε: ```latte link to Front:Shop:Product:show @@ -140,7 +140,7 @@ $url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); refresh ``` -Ταυτόχρονα, όλες οι παράμετροι που καθορίζονται στην υπογραφή της εντολής `render()` ή `action()` μεταφέρονται. Έτσι, αν βρισκόμαστε στις σελίδες `Product:show` και `id:123`, ο σύνδεσμος προς την `this` θα μεταφέρει και αυτή την παράμετρο. +Ταυτόχρονα, όλες οι παράμετροι που καθορίζονται στην υπογραφή της `action()` ή `render()` μεθόδου, εάν η `action()` δεν έχει οριστεί, μεταφέρονται. Έτσι, αν βρισκόμαστε στις σελίδες `Product:show` και `id:123`, ο σύνδεσμος προς την `this` θα μεταφέρει και αυτή την παράμετρο. Φυσικά, είναι δυνατόν να καθορίσετε τις παραμέτρους απευθείας: @@ -213,7 +213,7 @@ $url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); Αν θέλουμε να συνδέσουμε με παρουσιαστές στο πρότυπο συστατικού, χρησιμοποιούμε την ετικέτα `{plink}`: ```latte -home +home ``` ή στον κώδικα @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Ψευδώνυμα .[#toc-aliases]{data-version:v3.2.2} +============================================== + +Μερικές φορές είναι χρήσιμο να αντιστοιχίσετε ένα εύκολα απομνημονεύσιμο ψευδώνυμο σε ένα ζεύγος Παρουσιαστής:ενέργεια. Για παράδειγμα, θα μπορούσατε να ονομάσετε την αρχική σελίδα `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 +``` + +Στους συνδέσμους, γράφονται χρησιμοποιώντας το σύμβολο at, για παράδειγμα: + +```latte +administration +``` + +Υποστηρίζονται σε όλες τις μεθόδους που λειτουργούν με συνδέσμους, όπως το `redirect()` και παρόμοια. + + Άκυροι σύνδεσμοι .[#toc-invalid-links] ====================================== @@ -257,6 +281,6 @@ LinkGenerator .[#toc-linkgenerator] LinkGenerator είναι μια υπηρεσία που μπορείτε να έχετε περάσει μέσω του κατασκευαστή και στη συνέχεια να δημιουργήσετε συνδέσμους χρησιμοποιώντας τη μέθοδό του `link()`. -Υπάρχει μια διαφορά σε σχέση με τους παρουσιαστές. Το LinkGenerator δημιουργεί όλους τους συνδέσμους ως απόλυτες διευθύνσεις URL. Επιπλέον, δεν υπάρχει "τρέχων παρουσιαστής", οπότε δεν είναι δυνατόν να καθορίσετε μόνο το όνομα της ενέργειας `link('default')` ή τις σχετικές διαδρομές προς τις [ενότητες |modules]. +Σε σύγκριση με τους παρουσιαστές, υπάρχει διαφορά. Το LinkGenerator δημιουργεί όλους τους συνδέσμους απευθείας ως απόλυτες διευθύνσεις URL. Επίσης, δεν υπάρχει "πραγματικός παρουσιαστής", οπότε δεν μπορείτε απλώς να αναφέρετε το όνομα της ενέργειας `link('default')` ως στόχο ή να αναφέρετε σχετικές διαδρομές σε ενότητες. Οι άκυροι σύνδεσμοι πάντα προκαλούν `Nette\Application\UI\InvalidLinkException`. diff --git a/application/el/directory-structure.texy b/application/el/directory-structure.texy new file mode 100644 index 0000000000..d7fba83719 --- /dev/null +++ b/application/el/directory-structure.texy @@ -0,0 +1,526 @@ +Δομή καταλόγου της εφαρμογής +**************************** + +
    + +Πώς να σχεδιάσετε μια σαφή και κλιμακούμενη δομή καταλόγου για έργα στο Nette Framework; Θα σας δείξουμε δοκιμασμένες πρακτικές που θα σας βοηθήσουν να οργανώσετε τον κώδικά σας. Θα μάθετε: + +- πώς να **δομήσετε λογικά** την εφαρμογή σε καταλόγους +- πώς να σχεδιάζετε τη δομή ώστε να **κλιμακώνεται καλά** καθώς το έργο μεγαλώνει +- ποιες είναι οι **πιθανές εναλλακτικές λύσεις** και τα πλεονεκτήματα ή μειονεκτήματά τους + +
    + + +Είναι σημαντικό να αναφέρουμε ότι το ίδιο το Nette Framework δεν επιμένει σε κάποια συγκεκριμένη δομή. Είναι σχεδιασμένο ώστε να προσαρμόζεται εύκολα σε οποιεσδήποτε ανάγκες και προτιμήσεις. + + +Βασική δομή έργου .[#toc-basic-project-structure] +================================================= + +Αν και το 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 της. + + +Αρχές οργάνωσης κώδικα .[#toc-code-organization-principles] +=========================================================== + +Όταν εξερευνάτε για πρώτη φορά ένα νέο έργο, θα πρέπει να είστε σε θέση να προσανατολιστείτε γρήγορα. Φανταστείτε να κάνετε κλικ στον κατάλογο `app/Model/` και να δείτε αυτή τη δομή: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +Από αυτήν, θα μάθετε μόνο ότι το έργο χρησιμοποιεί κάποιες υπηρεσίες, αποθετήρια και οντότητες. Δεν θα μάθετε τίποτα για τον πραγματικό σκοπό της εφαρμογής. + +Ας δούμε μια διαφορετική προσέγγιση - **οργάνωση με βάση τους τομείς**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Αυτό είναι διαφορετικό - με την πρώτη ματιά είναι σαφές ότι πρόκειται για ιστότοπο ηλεκτρονικού εμπορίου. Τα ίδια τα ονόματα των καταλόγων αποκαλύπτουν τι μπορεί να κάνει η εφαρμογή - λειτουργεί με πληρωμές, παραγγελίες και προϊόντα. + +Η πρώτη προσέγγιση (οργάνωση με βάση τον τύπο της κλάσης) φέρνει αρκετά προβλήματα στην πράξη: ο κώδικας που σχετίζεται λογικά είναι διασκορπισμένος σε διαφορετικούς φακέλους και πρέπει να μεταπηδήσετε μεταξύ τους. Ως εκ τούτου, θα οργανώσουμε κατά τομείς. + + +Χώροι ονομάτων .[#toc-namespaces] +--------------------------------- + +Είναι συμβατικό η δομή καταλόγου να αντιστοιχεί σε χώρους ονομάτων στην εφαρμογή. Αυτό σημαίνει ότι η φυσική θέση των αρχείων αντιστοιχεί στο χώρο ονομάτων τους. Για παράδειγμα, μια κλάση που βρίσκεται στο `app/Model/Product/ProductRepository.php` θα πρέπει να έχει χώρο ονομάτων `App\Model\Product`. Αυτή η αρχή βοηθά στον προσανατολισμό του κώδικα και απλοποιεί την αυτόματη φόρτωση. + + +Ενικός και πληθυντικός αριθμός στα ονόματα .[#toc-singular-vs-plural-in-names] +------------------------------------------------------------------------------ + +Παρατηρήστε ότι χρησιμοποιούμε ενικό για τους κύριους καταλόγους εφαρμογών: `app`, `config`, `log`, `temp`, `www`. Το ίδιο ισχύει και στο εσωτερικό της εφαρμογής: `Model`, `Core`, `Presentation`. Αυτό συμβαίνει επειδή το καθένα αντιπροσωπεύει μια ενιαία έννοια. + +Ομοίως, το `app/Model/Product` αντιπροσωπεύει τα πάντα σχετικά με τα προϊόντα. Δεν το ονομάζουμε `Products` επειδή δεν είναι ένας φάκελος γεμάτος προϊόντα (που θα περιείχε αρχεία όπως `iphone.php`, `samsung.php`). Είναι ένας χώρος ονομάτων που περιέχει κλάσεις για την εργασία με προϊόντα - `ProductRepository.php`, `ProductService.php`. + +Ο φάκελος `app/Tasks` είναι στον πληθυντικό επειδή περιέχει ένα σύνολο αυτόνομων εκτελέσιμων σεναρίων - `CleanupTask.php`, `ImportTask.php`. Κάθε ένα από αυτά αποτελεί μια ανεξάρτητη μονάδα. + +Για λόγους συνέπειας, συνιστούμε να χρησιμοποιείτε: +- Ενικός για χώρους ονομάτων που αντιπροσωπεύουν μια λειτουργική μονάδα (ακόμη και αν εργάζεστε με πολλαπλές οντότητες) +- Πληθυντικός για συλλογές ανεξάρτητων μονάδων +- Σε περίπτωση αβεβαιότητας ή αν δεν θέλετε να το σκεφτείτε, επιλέξτε τον ενικό + + +Δημόσιος κατάλογος `www/` .[#toc-public-directory-www] +====================================================== + +Αυτός ο κατάλογος είναι ο μόνος προσβάσιμος από το διαδίκτυο (το λεγόμενο document-root). Συχνά μπορεί να συναντήσετε το όνομα `public/` αντί για `www/` - είναι απλώς θέμα σύμβασης και δεν επηρεάζει τη λειτουργικότητα. Ο κατάλογος περιέχει: +- [Σημείο εισόδου |bootstrap#index.php] της εφαρμογής `index.php` +- αρχείο `.htaccess` με κανόνες mod_rewrite (για τον Apache) +- Στατικά αρχεία (CSS, JavaScript, εικόνες) +- Ανεβασμένα αρχεία + +Για τη σωστή ασφάλεια της εφαρμογής, είναι ζωτικής σημασίας να έχετε [ρυθμίσει |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url] σωστά [το document-root |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url]. + +.[note] +Ποτέ μην τοποθετείτε το φάκελο `node_modules/` σε αυτόν τον κατάλογο - περιέχει χιλιάδες αρχεία που μπορεί να είναι εκτελέσιμα και δεν πρέπει να είναι προσβάσιμα από το κοινό. + + +Κατάλογος εφαρμογών `app/` .[#toc-application-directory-app] +============================================================ + +Αυτός είναι ο κύριος κατάλογος με τον κώδικα της εφαρμογής. Βασική δομή: + +/--pre +app/ +├── Core/ ← θέματα υποδομής +├── Model/ ← επιχειρησιακή λογική +├── Presentation/ ← παρουσιαστές και πρότυπα +├── Tasks/ ← σενάρια εντολών +└── Bootstrap.php ← κλάση bootstrap της εφαρμογής +\-- + +`Bootstrap.php` είναι η [κλάση εκκίνησης της εφαρμογής |bootstrap] που αρχικοποιεί το περιβάλλον, φορτώνει τη διαμόρφωση και δημιουργεί το δοχείο DI. + +Ας δούμε τώρα αναλυτικά τους επιμέρους υποκαταλόγους. + + +Παρουσιαστές και πρότυπα .[#toc-presenters-and-templates] +========================================================= + +Έχουμε το τμήμα παρουσίασης της εφαρμογής στον κατάλογο `app/Presentation`. Μια εναλλακτική λύση είναι το σύντομο `app/UI`. Εκεί βρίσκονται όλοι οι παρουσιαστές, τα πρότυπα τους και τυχόν βοηθητικές κλάσεις. + +Οργανώνουμε αυτό το επίπεδο ανά τομείς. Σε ένα σύνθετο έργο που συνδυάζει ηλεκτρονικό εμπόριο, blog και API, η δομή θα έμοιαζε ως εξής: + +/--pre +app/Presentation/ +├── Shop/ ← frontend ηλεκτρονικού εμπορίου +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← blog +│ ├── Home/ +│ └── Post/ +├── Admin/ ← διαχείριση +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← Τελικά σημεία API + └── V1/ +\-- + +Αντίθετα, για ένα απλό ιστολόγιο θα χρησιμοποιούσαμε αυτή τη δομή: + +/--pre +app/Presentation/ +├── Front/ ← εμπρόσθιο άκρο ιστοσελίδας +│ ├── Home/ +│ └── Post/ +├── Admin/ ← διαχείριση +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, sitemaps κ.λπ. +\-- + +Οι φάκελοι όπως `Home/` ή `Dashboard/` περιέχουν παρουσιαστές και πρότυπα. Οι φάκελοι όπως `Front/`, `Admin/` ή `Api/` ονομάζονται **modules**. Τεχνικά, πρόκειται για κανονικούς καταλόγους που χρησιμεύουν για τη λογική οργάνωση της εφαρμογής. + +Κάθε φάκελος με έναν παρουσιαστή περιέχει έναν παρουσιαστή με παρόμοιο όνομα και τα πρότυπά του. Για παράδειγμα, ο φάκελος `Dashboard/` περιέχει: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← παρουσιαστής +└── default.latte ← πρότυπο +\-- + +Αυτή η δομή καταλόγων αντικατοπτρίζεται στους χώρους ονομάτων των κλάσεων. Για παράδειγμα, το `DashboardPresenter` βρίσκεται στο χώρο ονομάτων `App\Presentation\Admin\Dashboard` (βλ. [αντιστοίχιση παρουσιαστή |#presenter mapping]): + +```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`. + + +Ανάπτυξη ευέλικτης δομής .[#toc-flexible-structure-development] +--------------------------------------------------------------- + +Ένα από τα μεγάλα πλεονεκτήματα αυτής της δομής είναι το πόσο κομψά προσαρμόζεται στις αυξανόμενες ανάγκες του έργου. Ως παράδειγμα, ας πάρουμε το τμήμα που παράγει XML feeds. Αρχικά, έχουμε μια απλή φόρμα: + +/--pre +Export/ +├── ExportPresenter.php ← ένας παρουσιαστής για όλες τις εξαγωγές +├── sitemap.latte ← πρότυπο για χάρτη σελίδων +└── feed.latte ← πρότυπο για RSS feed +\-- + +Με την πάροδο του χρόνου, προστίθενται περισσότεροι τύποι τροφοδοσίας και χρειαζόμαστε περισσότερη λογική για αυτούς... Κανένα πρόβλημα! Ο φάκελος `Export/` γίνεται απλά μια ενότητα: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← feed για την Amazon + └── ebay.latte ← τροφοδοσία για το eBay +\-- + +Απλά δημιουργήστε νέους υποφακέλους, χωρίστε τον κώδικα σε αυτούς και ενημερώστε τους συνδέσμους (π.χ. από `Export:feed` σε `Export:Feed:amazon`). Χάρη σε αυτό, μπορούμε να επεκτείνουμε σταδιακά τη δομή όπως χρειάζεται, το επίπεδο φωλιάσματος δεν περιορίζεται με κανέναν τρόπο. + +Για παράδειγμα, αν στη διαχείριση έχετε πολλούς παρουσιαστές που σχετίζονται με τη διαχείριση παραγγελιών, όπως `OrderDetail`, `OrderEdit`, `OrderDispatch` κ.λπ. μπορείτε να δημιουργήσετε μια ενότητα (φάκελο) `Order` για καλύτερη οργάνωση, η οποία θα περιέχει (φακέλους για) τους παρουσιαστές `Detail`, `Edit`, `Dispatch` και άλλους. + + +Τοποθεσία προτύπου .[#toc-template-location] +-------------------------------------------- + +Στα προηγούμενα παραδείγματα, είδαμε ότι τα πρότυπα βρίσκονται απευθείας στο φάκελο με τον παρουσιαστή: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← παρουσιαστής +├── DashboardTemplate.php ← προαιρετική κλάση προτύπου +└── default.latte ← πρότυπο +\-- + +Αυτή η τοποθεσία αποδεικνύεται η πιο βολική στην πράξη - έχετε όλα τα σχετικά αρχεία στη διάθεσή σας. + +Εναλλακτικά, μπορείτε να τοποθετήσετε τα πρότυπα σε έναν υποφάκελο `templates/`. Η Nette υποστηρίζει και τις δύο παραλλαγές. Μπορείτε ακόμη και να τοποθετήσετε τα πρότυπα εντελώς εκτός του φακέλου `Presentation/`. Τα πάντα σχετικά με τις επιλογές τοποθέτησης προτύπων μπορείτε να βρείτε στο κεφάλαιο [Αναζήτηση προτύπων |templates#Template Lookup]. + + +Βοηθητικές κλάσεις και συστατικά .[#toc-helper-classes-and-components] +---------------------------------------------------------------------- + +Οι παρουσιαστές και τα πρότυπα συχνά συνοδεύονται από άλλα βοηθητικά αρχεία. Τα τοποθετούμε λογικά ανάλογα με το πεδίο εφαρμογής τους: + +1. **Απευθείας με τον παρουσιαστή** σε περίπτωση που πρόκειται για συγκεκριμένα στοιχεία για τον συγκεκριμένο παρουσιαστή: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← συστατικό για την καταχώριση προϊόντων +└── FilterForm.php ← φόρμα για φιλτράρισμα +\-- + +2. **Για την ενότητα** - συνιστούμε τη χρήση του φακέλου `Accessory`, ο οποίος τοποθετείται τακτοποιημένα στην αρχή του αλφαβήτου: + +/--pre +Front/ +├── Accessory/ +│ ├── NavbarControl.php ← συστατικά για 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/`. Και τα συστατικά στον φάκελο `app/Components`. Η επιλογή εξαρτάται από τις συμβάσεις της ομάδας. + + +Μοντέλο - Η καρδιά της εφαρμογής .[#toc-model-heart-of-the-application] +======================================================================= + +Το μοντέλο περιέχει όλη την επιχειρησιακή λογική της εφαρμογής. Για την οργάνωσή του, ισχύει ο ίδιος κανόνας - δομούμε με βάση τους τομείς: + +/--pre +app/Model/ +├── Payment/ ← τα πάντα για τις πληρωμές +│ ├── PaymentFacade.php ← κύριο σημείο εισόδου +│ ├── PaymentRepository.php +│ ├── Payment.php ← οντότητα +├── Order/ ← τα πάντα για τις παραγγελίες +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← τα πάντα για τη ναυτιλία +\-- + +Στο μοντέλο, συναντάτε συνήθως αυτούς τους τύπους κλάσεων: + +**Προσοψεις**: αντιπροσωπεύουν το κύριο σημείο εισόδου σε ένα συγκεκριμένο τομέα της εφαρμογής. Λειτουργούν ως ενορχηστρωτής που συντονίζει τη συνεργασία μεταξύ διαφορετικών υπηρεσιών για την υλοποίηση ολοκληρωμένων περιπτώσεων χρήσης (όπως "δημιουργία παραγγελίας" ή "επεξεργασία πληρωμής"). Κάτω από το επίπεδο ενορχήστρωσης, η όψη αποκρύπτει τις λεπτομέρειες υλοποίησης από την υπόλοιπη εφαρμογή, παρέχοντας έτσι μια καθαρή διεπαφή για την εργασία με τον συγκεκριμένο τομέα. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // επικύρωση + // δημιουργία παραγγελίας + // αποστολή email + // εγγραφή σε στατιστικά στοιχεία + } +} +``` + +**Υπηρεσίες**: εστιάζουν σε συγκεκριμένες επιχειρηματικές λειτουργίες εντός ενός τομέα. Σε αντίθεση με τις προσόψεις που ενορχηστρώνουν ολόκληρες περιπτώσεις χρήσης, μια υπηρεσία υλοποιεί συγκεκριμένη επιχειρηματική λογική (όπως υπολογισμούς τιμών ή επεξεργασία πληρωμών). Οι υπηρεσίες είναι συνήθως χωρίς κατάσταση και μπορούν να χρησιμοποιηθούν είτε από τις προσόψεις ως δομικά στοιχεία για πιο σύνθετες λειτουργίες, είτε απευθείας από άλλα μέρη της εφαρμογής για απλούστερες εργασίες. + +```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 +// Οντότητα που αντιστοιχίζεται στον πίνακα παραγγελιών της βάσης δεδομένων +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, + ]); + } +} +``` + +**Αντικείμενα αξίας**: αμετάβλητα αντικείμενα που αντιπροσωπεύουν τιμές χωρίς δική τους ταυτότητα - για παράδειγμα, ένα χρηματικό ποσό ή μια διεύθυνση ηλεκτρονικού ταχυδρομείου. Δύο περιπτώσεις ενός αντικειμένου αξίας με τις ίδιες τιμές θεωρούνται πανομοιότυπες. + + +Κώδικας υποδομής .[#toc-infrastructure-code] +============================================ + +Ο φάκελος `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` για την αποστολή μηνυμάτων ηλεκτρονικού ταχυδρομείου σχετικά με τις παραγγελίες, γνωρίζει τα πρότυπα τους και πότε πρέπει να σταλούν + + +Σενάρια εντολών .[#toc-command-scripts] +======================================= + +Οι εφαρμογές πρέπει συχνά να εκτελούν εργασίες εκτός των κανονικών αιτήσεων HTTP - είτε πρόκειται για επεξεργασία δεδομένων στο παρασκήνιο, είτε για συντήρηση, είτε για περιοδικές εργασίες. Για την εκτέλεση χρησιμοποιούνται απλά σενάρια στον κατάλογο `bin/`, ενώ η πραγματική λογική της εφαρμογής τοποθετείται στον κατάλογο `app/Tasks/` (ή `app/Commands/`). + +Παράδειγμα: + +/--pre +app/Tasks/ +├── Maintenance/ ← σενάρια συντήρησης +│ ├── CleanupCommand.php ← διαγραφή παλαιών δεδομένων +│ └── DbOptimizeCommand.php ← βελτιστοποίηση βάσης δεδομένων +├── Integration/ ← ενσωμάτωση με εξωτερικά συστήματα +│ ├── ImportProducts.php ← εισαγωγή από σύστημα προμηθευτή +│ └── SyncOrders.php ← συγχρονισμός παραγγελιών +└── Scheduled/ ← τακτικές εργασίες + ├── NewsletterCommand.php ← αποστολή ενημερωτικών δελτίων + └── ReminderCommand.php ← ειδοποιήσεις πελατών +\-- + +Τι ανήκει στο μοντέλο και τι στα σενάρια εντολών; Για παράδειγμα, η λογική για την αποστολή ενός email ανήκει στο μοντέλο, η μαζική αποστολή χιλιάδων email ανήκει στο `Tasks/`. + +Οι εργασίες [εκτελούνται |https://blog.nette.org/en/cli-scripts-in-nette-application] συνήθως [από τη γραμμή εντολών |https://blog.nette.org/en/cli-scripts-in-nette-application] ή μέσω cron. Μπορούν επίσης να εκτελούνται μέσω αίτησης HTTP, αλλά πρέπει να ληφθεί υπόψη η ασφάλεια. Ο παρουσιαστής που εκτελεί την εργασία πρέπει να είναι ασφαλής, π.χ. μόνο για συνδεδεμένους χρήστες ή με ένα ισχυρό token και πρόσβαση από επιτρεπόμενες διευθύνσεις IP. Για εργασίες μεγάλης διάρκειας, είναι απαραίτητο να αυξήσετε το χρονικό όριο της δέσμης ενεργειών και να χρησιμοποιήσετε το `session_write_close()` για να αποφύγετε το κλείδωμα της συνεδρίας. + + +Άλλοι πιθανοί κατάλογοι .[#toc-other-possible-directories] +========================================================== + +Εκτός από τους αναφερόμενους βασικούς καταλόγους, μπορείτε να προσθέσετε και άλλους εξειδικευμένους φακέλους ανάλογα με τις ανάγκες του έργου. Ας δούμε τους πιο συνηθισμένους και τη χρήση τους: + +/--pre +app/ +├── Api/ ← Λογική API ανεξάρτητη από το επίπεδο παρουσίασης +├── Database/ ← σενάρια μετάβασης και σπορείς για δεδομένα δοκιμών +├── Components/ ← κοινά οπτικά στοιχεία σε όλη την εφαρμογή +├── Event/ ← χρήσιμη αν χρησιμοποιείται αρχιτεκτονική με γνώμονα το γεγονός +├── Mail/ ← πρότυπα ηλεκτρονικού ταχυδρομείου και σχετική λογική +└── Utils/ ← βοηθητικές κλάσεις +\-- + +Για κοινά οπτικά στοιχεία που χρησιμοποιούνται σε παρουσιαστές σε όλη την εφαρμογή, μπορείτε να χρησιμοποιήσετε το φάκελο `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/ ← πρότυπα email +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Χαρτογράφηση παρουσιαστή .[#toc-presenter-mapping] +================================================== + +Η αντιστοίχιση ορίζει κανόνες για την εξαγωγή ονομάτων κλάσεων από ονόματα παρουσιαστών. Τους καθορίζουμε στη [διαμόρφωση |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 +``` + +Η αντιστοίχιση λειτουργεί με την αντικατάσταση του αστερίσκου στη μάσκα `App\Presentation\*Presenter` με το όνομα του παρουσιαστή `Home`, με αποτέλεσμα το τελικό όνομα της κλάσης `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/el/how-it-works.texy b/application/el/how-it-works.texy index 2cfe6c52b9..66270824a6 100644 --- a/application/el/how-it-works.texy +++ b/application/el/how-it-works.texy @@ -22,18 +22,18 @@ /--pre web-project/ ├── app/ ← directory with application -│ ├── Presenters/ ← presenter classes -│ │ ├── HomePresenter.php ← Home presenter class -│ │ └── templates/ ← templates directory -│ │ ├── @layout.latte ← template of shared layout -│ │ └── Home/ ← templates for Home presenter -│ │ └── default.latte ← template for action `default` -│ ├── Router/ ← configuration of URL addresses +│ ├── Core/ ← βασικές αναγκαίες τάξεις +│ │ └── RouterFactory.php ← διαμόρφωση των διευθύνσεων URL +│ ├── Presentation/ ← παρουσιαστές, πρότυπα και λοιπά. +│ │ ├── @layout.latte ← πρότυπο κοινής διάταξης +│ │ └── Home/ ← Αρχικός κατάλογος παρουσιαστών +│ │ ├── HomePresenter.php ← Κλάση οικιακού παρουσιαστή +│ │ └── default.latte ← πρότυπο για τη δράση default │ └── Bootstrap.php ← booting class Bootstrap ├── bin/ ← scripts for the command line ├── config/ ← configuration files │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← error logs ├── temp/ ← temporary files, cache, … ├── vendor/ ← libraries installed by Composer @@ -45,9 +45,9 @@ └── .htaccess ← prohibits access to all directories except www \-- -Μπορείτε να αλλάξετε τη δομή του καταλόγου με οποιονδήποτε τρόπο, να μετονομάσετε ή να μετακινήσετε φακέλους και στη συνέχεια να επεξεργαστείτε απλώς τις διαδρομές προς τα `log/` και `temp/` στο αρχείο `Bootstrap.php` και τη διαδρομή προς αυτό το αρχείο στο `composer.json` στο τμήμα `autoload`. Τίποτα περισσότερο, καμία περίπλοκη αναδιαμόρφωση, καμία συνεχής αλλαγή. Το Nette διαθέτει μια [έξυπνη αυτόματη αναγνώριση |bootstrap#development-vs-production-mode]. +Μπορείτε να τροποποιήσετε τη δομή του καταλόγου όπως θέλετε, να μετονομάσετε ή να μετακινήσετε φακέλους - είναι απολύτως ευέλικτο. Το Nette διαθέτει επίσης έξυπνη αυτόματη ανίχνευση και αναγνωρίζει αυτόματα τη θέση της εφαρμογής, συμπεριλαμβανομένης της βάσης URL της. -Για λίγο μεγαλύτερες εφαρμογές, μπορούμε να χωρίσουμε τους φακέλους με τους παρουσιαστές και τα πρότυπα σε υποκαταλόγους (στο δίσκο) και σε χώρους ονομάτων (στον κώδικα), τους οποίους ονομάζουμε [ενότητες |modules]. +Για λίγο μεγαλύτερες εφαρμογές, μπορούμε να οργανώσουμε τους φακέλους παρουσιαστή και προτύπων σε [υποκαταλόγους |directory-structure#Presenters and templates] και να ομαδοποιήσουμε τις κλάσεις σε χώρους ονομάτων, τους οποίους ονομάζουμε ενότητες. Ο κατάλογος `www/` είναι ο δημόσιος κατάλογος ή το document-root του έργου. Μπορείτε να τον μετονομάσετε χωρίς να χρειάζεται να ορίσετε κάτι άλλο στην πλευρά της εφαρμογής. Απλά πρέπει να [ρυθμίσετε τη φιλοξενία |nette:troubleshooting#How to change or remove www directory from URL] έτσι ώστε το document-root να πηγαίνει σε αυτόν τον κατάλογο. @@ -75,7 +75,7 @@ composer create-project nette/web-project Τι είδους εργοστάσιο; Δεν παράγουμε τρακτέρ, αλλά ιστοσελίδες! Περιμένετε, θα σας εξηγήσω αμέσως. -Με τον όρο "αρχικοποίηση του περιβάλλοντος" εννοούμε, για παράδειγμα, ότι ενεργοποιείται το [Tracy |tracy:], το οποίο είναι ένα καταπληκτικό εργαλείο για την καταγραφή ή την οπτικοποίηση σφαλμάτων. Καταγράφει τα σφάλματα στον διακομιστή παραγωγής και τα εμφανίζει απευθείας στον διακομιστή ανάπτυξης. Επομένως, η αρχικοποίηση πρέπει επίσης να αποφασίσει αν ο ιστότοπος εκτελείται σε κατάσταση παραγωγής ή ανάπτυξης. Για να το κάνει αυτό, η Nette χρησιμοποιεί αυτόματη ανίχνευση: αν τρέξετε τον ιστότοπο στο localhost, τρέχει σε λειτουργία προγραμματιστή. Δεν χρειάζεται να ρυθμίσετε τίποτα και η εφαρμογή είναι έτοιμη τόσο για ανάπτυξη όσο και για ανάπτυξη παραγωγής. Αυτά τα βήματα εκτελούνται και περιγράφονται λεπτομερώς στο κεφάλαιο σχετικά με την [κλάση Bootstrap |bootstrap]. +Με τον όρο "αρχικοποίηση περιβάλλοντος" εννοούμε, για παράδειγμα, την ενεργοποίηση [του Tracy |tracy:], το οποίο είναι ένα φανταστικό εργαλείο για την καταγραφή και την απεικόνιση σφαλμάτων. Στους διακομιστές παραγωγής καταγράφει τα σφάλματα, ενώ στους διακομιστές ανάπτυξης τα εμφανίζει απευθείας. Επομένως, η αρχικοποίηση περιλαμβάνει τον καθορισμό του αν ο ιστότοπος εκτελείται σε κατάσταση παραγωγής ή ανάπτυξης. Για το σκοπό αυτό, η Nette χρησιμοποιεί [έξυπνη αυτόματη ανίχνευση |bootstrap#development-vs-production-mode]: αν τρέχετε τον ιστότοπο σε localhost, λειτουργεί σε κατάσταση ανάπτυξης. Δεν απαιτείται καμία διαμόρφωση και η εφαρμογή είναι έτοιμη τόσο για ανάπτυξη όσο και για παραγωγή. Αυτά τα βήματα εκτελούνται και περιγράφονται λεπτομερώς στο κεφάλαιο της [κλάσης Bootstrap |bootstrap]. Το τρίτο σημείο (ναι, παραλείψαμε το δεύτερο, αλλά θα επανέλθουμε σε αυτό) είναι η εκκίνηση της εφαρμογής. Ο χειρισμός των αιτημάτων HTTP στη Nette γίνεται από την κλάση `Nette\Application\Application` (στο εξής θα αναφέρεται ως `Application`), οπότε όταν λέμε "τρέχουμε μια εφαρμογή", εννοούμε να καλέσουμε μια μέθοδο με το όνομα `run()` σε ένα αντικείμενο αυτής της κλάσης. @@ -91,7 +91,7 @@ composer create-project nette/web-project Η εφαρμογή ξεκινά ζητώντας από τον λεγόμενο δρομολογητή να αποφασίσει ποιος από τους παρουσιαστές θα περάσει το τρέχον αίτημα για επεξεργασία. Ο δρομολογητής αποφασίζει ποιανού ευθύνη είναι. Κοιτάζει τη διεύθυνση URL εισόδου `https://example.com/product/123`, ο οποίος θέλει να `show` ένα προϊόν με `id: 123` ως ενέργεια. Είναι καλή συνήθεια να γράφετε τα ζεύγη παρουσιαστής + δράση χωρισμένα με άνω και κάτω τελεία ως `Product:show`. -Έτσι, ο δρομολογητής μετατρέπει τη διεύθυνση URL σε ένα ζεύγος `Presenter:action` + παράμετροι, στην περίπτωσή μας `Product:show` + `id: 123`. Μπορείτε να δείτε πώς μοιάζει ένας δρομολογητής στο αρχείο `app/Router/RouterFactory.php` και θα τον περιγράψουμε αναλυτικά στο κεφάλαιο [Δρομολόγηση |Routing]. +Έτσι, ο δρομολογητής μετατρέπει τη διεύθυνση URL σε ένα ζεύγος `Presenter:action` + παράμετροι, στην περίπτωσή μας `Product:show` + `id: 123`. Μπορείτε να δείτε πώς μοιάζει ένας δρομολογητής στο αρχείο `app/Core/RouterFactory.php` και θα τον περιγράψουμε αναλυτικά στο κεφάλαιο [Δρομολόγηση |Routing]. Ας συνεχίσουμε. Η εφαρμογή γνωρίζει ήδη το όνομα του παρουσιαστή και μπορεί να συνεχίσει. Δημιουργώντας ένα αντικείμενο `ProductPresenter`, το οποίο είναι ο κώδικας του παρουσιαστή `Product`. Πιο συγκεκριμένα, ζητάει από το DI container τη δημιουργία του presenter, επειδή η παραγωγή αντικειμένων είναι η δουλειά του. @@ -121,12 +121,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Στη συνέχεια, ο παρουσιαστής επιστρέφει την απάντηση. Αυτό μπορεί να είναι μια σελίδα HTML, μια εικόνα, ένα έγγραφο XML, η αποστολή ενός αρχείου από το δίσκο, JSON ή η ανακατεύθυνση σε μια άλλη σελίδα. Σημαντικό είναι ότι, αν δεν πούμε ρητά πώς να απαντήσουμε (κάτι που συμβαίνει στην περίπτωση του `ProductPresenter`), η απάντηση θα είναι η απόδοση του προτύπου με μια σελίδα HTML. Γιατί; Λοιπόν, επειδή στο 99% των περιπτώσεων θέλουμε να σχεδιάσουμε ένα πρότυπο, οπότε ο παρουσιαστής θεωρεί αυτή τη συμπεριφορά ως προεπιλεγμένη και θέλει να διευκολύνει τη δουλειά μας. Αυτό είναι το νόημα της Nette. -Δεν χρειάζεται καν να δηλώσουμε ποιο πρότυπο θέλουμε να σχεδιάσουμε, αυτός εξάγει τη διαδρομή προς αυτό σύμφωνα με απλή λογική. Στην περίπτωση του presenter `Product` και της δράσης `show`, προσπαθεί να δει αν ένα από αυτά τα αρχεία προτύπων υπάρχει σε σχέση με τον κατάλογο όπου βρίσκεται η κλάση `ProductPresenter`: +Δεν χρειάζεται καν να καθορίσουμε ποιο πρότυπο θα αναπαραχθεί- το πλαίσιο θα βρει μόνο του τη διαδρομή. Στην περίπτωση της ενέργειας `show`, απλώς προσπαθεί να φορτώσει το πρότυπο `show.latte` στον κατάλογο με την κλάση `ProductPresenter`. Προσπαθεί επίσης να βρει τη διάταξη στο αρχείο `@layout.latte` (περισσότερα για την [αναζήτηση προτύπων |templates#Template Lookup]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -Θα προσπαθήσει επίσης να βρει τη διάταξη στο αρχείο `@layout.latte` και στη συνέχεια θα αποδώσει το πρότυπο. Τώρα ολοκληρώνεται η εργασία του παρουσιαστή και ολόκληρης της εφαρμογής. Εάν το πρότυπο δεν υπάρχει, θα επιστραφεί μια σελίδα με σφάλμα 404. Μπορείτε να διαβάσετε περισσότερα για τους παρουσιαστές στη σελίδα [Παρουσιαστές |Presenters]. +Στη συνέχεια, τα πρότυπα αποδίδονται. Με αυτόν τον τρόπο ολοκληρώνεται η εργασία του παρουσιαστή και ολόκληρης της εφαρμογής, και η εργασία έχει τελειώσει. Εάν το πρότυπο δεν υπήρχε, θα επιστρεφόταν μια σελίδα σφάλματος 404. Μπορείτε να διαβάσετε περισσότερα για τους παρουσιαστές στη σελίδα [Παρουσιαστές |presenters]. [* request-flow.svg *] @@ -137,7 +134,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter 3) ο δρομολογητής αποκωδικοποιεί τη διεύθυνση URL ως ζεύγος `Home:default` 4) δημιουργείται ένα αντικείμενο `HomePresenter` 5) καλείται η μέθοδος `renderDefault()` (αν υπάρχει) -6) αποδίδεται ένα πρότυπο `templates/Home/default.latte` με διάταξη `templates/@layout.latte` +6) αποδίδεται ένα πρότυπο `default.latte` με διάταξη `@layout.latte` Μπορεί να έχετε συναντήσει πολλές νέες έννοιες τώρα, αλλά πιστεύουμε ότι βγάζουν νόημα. Η δημιουργία εφαρμογών στη Nette είναι πανεύκολη. diff --git a/application/el/modules.texy b/application/el/modules.texy deleted file mode 100644 index b2bb1dffd0..0000000000 --- a/application/el/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Ενότητες -******** - -.[perex] -Στη Nette, οι ενότητες αντιπροσωπεύουν τις λογικές μονάδες που συνθέτουν μια εφαρμογή. Περιλαμβάνουν παρουσιαστές, πρότυπα, ενδεχομένως επίσης συστατικά και κλάσεις μοντέλων. - -Ένας κατάλογος για τους παρουσιαστές και ένας για τα πρότυπα δεν θα ήταν αρκετός για πραγματικά έργα. Το να έχετε δεκάδες αρχεία σε έναν φάκελο είναι τουλάχιστον ανοργάνωτο. Πώς να απαλλαγείτε από αυτό; Απλώς τα χωρίζουμε σε υποκαταλόγους στο δίσκο και σε χώρους ονομάτων στον κώδικα. Και αυτό ακριβώς κάνουν τα modules της Nette. - -Ας ξεχάσουμε λοιπόν έναν ενιαίο φάκελο για τους παρουσιαστές και τα πρότυπα και ας δημιουργήσουμε αντ' αυτού ενότητες, για παράδειγμα `Admin` και `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← directory with modules -│ ├── Admin/ ← module Admin -│ │ ├── Presenters/ ← its presenters -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← module Front -│ └── Presenters/ ← its presenters -│ └── ... -\-- - -Αυτή η δομή καταλόγου θα αντικατοπτρίζεται από τα namespaces των κλάσεων, έτσι για παράδειγμα το `DashboardPresenter` θα βρίσκεται στο namespace `App\Modules\Admin\Presenters`: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Ο παρουσιαστής `Dashboard` μέσα στην ενότητα `Admin` αναφέρεται μέσα στην εφαρμογή χρησιμοποιώντας τον συμβολισμό της άνω και κάτω τελείας ως `Admin:Dashboard`, και η ενέργεια `default` ως `Admin:Dashboard:default`. -Και πώς γνωρίζει η Nette proper ότι το `Admin:Dashboard` αντιπροσωπεύει την κλάση `App\Modules\Admin\Presenters\DashboardPresenter`; Αυτό καθορίζεται από την [αντιστοίχιση |#mapping] στη διαμόρφωση. -Έτσι, η δεδομένη δομή δεν είναι αυστηρά καθορισμένη και μπορείτε να την τροποποιήσετε ανάλογα με τις ανάγκες σας. - -Οι ενότητες μπορούν φυσικά να περιέχουν όλα τα άλλα στοιχεία εκτός από τους παρουσιαστές και τα πρότυπα, όπως συστατικά, κλάσεις μοντέλων κ.λπ. - - -Ενσωματωμένες ενότητες .[#toc-nested-modules] ---------------------------------------------- - -Οι ενότητες δεν χρειάζεται να σχηματίζουν μόνο μια επίπεδη δομή, μπορείτε επίσης να δημιουργήσετε υποενότητες, για παράδειγμα: - -/--pre -app/ -├── Modules/ ← directory with modules -│ ├── Blog/ ← module Blog -│ │ ├── Admin/ ← submodule Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← submodule Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← module Forum -│ │ └── ... -\-- - -Έτσι, η ενότητα `Blog` χωρίζεται σε υποενότητες `Admin` και `Front`. Και πάλι, αυτό θα αντικατοπτρίζεται στα namespaces, τα οποία θα είναι `App\Modules\Blog\Admin\Presenters` κ.λπ. Ο παρουσιαστής `Dashboard` μέσα στην υποενότητα αναφέρεται ως `Blog:Admin:Dashboard`. - -Η ένθεση μπορεί να προχωρήσει όσο βαθιά θέλετε, οπότε μπορούν να δημιουργηθούν υπο-υποενότητες. - - -Δημιουργία συνδέσμων .[#toc-creating-links] -------------------------------------------- - -Οι σύνδεσμοι στα πρότυπα παρουσιαστή είναι σχετικοί με την τρέχουσα ενότητα. Έτσι, ο σύνδεσμος `Foo:default` οδηγεί στον παρουσιαστή `Foo` στην ίδια ενότητα με τον τρέχοντα παρουσιαστή. Εάν η τρέχουσα ενότητα είναι `Front`, για παράδειγμα, τότε ο σύνδεσμος έχει ως εξής: - -```latte -link to Front:Product:show -``` - -Ένας σύνδεσμος είναι σχετικός ακόμη και αν περιλαμβάνει το όνομα μιας ενότητας, η οποία τότε θεωρείται υποενότητα: - -```latte -link to Front:Shop:Product:show -``` - -Οι απόλυτοι σύνδεσμοι γράφονται ανάλογα με τις απόλυτες διαδρομές στο δίσκο, αλλά με άνω και κάτω τελεία αντί για κάθετους. Έτσι, ένας απόλυτος σύνδεσμος αρχίζει με άνω και κάτω τελεία: - -```latte -link to Admin:Product:show -``` - -Για να μάθουμε αν βρισκόμαστε σε μια συγκεκριμένη ενότητα ή υποενότητά της μπορούμε να χρησιμοποιήσουμε τη συνάρτηση `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Δρομολόγηση .[#toc-routing] ---------------------------- - -Βλέπε [κεφάλαιο για τη δρομολόγηση |routing#Modules]. - - -Χαρτογράφηση .[#toc-mapping] ----------------------------- - -Καθορίζει τους κανόνες με τους οποίους το όνομα της κλάσης προκύπτει από το όνομα του παρουσιαστή. Τους γράφουμε στη [διαμόρφωση |configuration] κάτω από το κλειδί `application › mapping`. - -Ας ξεκινήσουμε με ένα δείγμα που δεν χρησιμοποιεί ενότητες. Θα θέλουμε απλώς οι κλάσεις presenter να έχουν το namespace `App\Presenters`. Αυτό σημαίνει ότι ένας παρουσιαστής όπως το `Home` θα πρέπει να αντιστοιχίζεται στην κλάση `App\Presenters\HomePresenter`. Αυτό μπορεί να επιτευχθεί με την ακόλουθη διαμόρφωση: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -Το όνομα του παρουσιαστή αντικαθίσταται με τον αστερίσκο στη μάσκα κλάσης και το αποτέλεσμα είναι το όνομα της κλάσης. Εύκολο! - -Αν χωρίσουμε τους παρουσιαστές σε ενότητες, μπορούμε να έχουμε τη δική μας χαρτογράφηση για κάθε ενότητα: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Τώρα ο παρουσιαστής `Front:Home` αντιστοιχίζεται στην κλάση `App\Modules\Front\Presenters\HomePresenter` και ο παρουσιαστής `Admin:Dashboard` στην κλάση `App\Modules\Admin\Presenters\DashboardPresenter`. - -Είναι πιο πρακτικό να δημιουργήσετε έναν γενικό κανόνα (αστέρι) για να αντικαταστήσετε τους δύο πρώτους. Ο επιπλέον αστερίσκος θα προστεθεί στη μάσκα κλάσης μόνο για την ενότητα: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Τι γίνεται όμως αν χρησιμοποιούμε φωλιασμένες ενότητες και έχουμε έναν παρουσιαστή `Admin:User:Edit`; Σε αυτή την περίπτωση, το τμήμα με τον αστερίσκο που αντιπροσωπεύει την ενότητα για κάθε επίπεδο απλώς επαναλαμβάνεται και το αποτέλεσμα είναι η κλάση `App\Modules\Admin\User\Presenters\EditPresenter`. - -Ένας εναλλακτικός συμβολισμός είναι η χρήση ενός πίνακα που αποτελείται από τρία τμήματα αντί για συμβολοσειρά. Αυτή η σημειογραφία είναι ισοδύναμη με την προηγούμενη: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -Η προεπιλεγμένη τιμή είναι `*: *Module\*Presenter`. diff --git a/application/el/presenters.texy b/application/el/presenters.texy index 07cb8b4482..8b7a737672 100644 --- a/application/el/presenters.texy +++ b/application/el/presenters.texy @@ -60,7 +60,7 @@ class ArticlePresenter extends Nette\Application\UI\Presenter Είναι σημαντικό ότι `action()` καλείται πριν από την `render()`, ώστε μέσα σε αυτό να μπορούμε ενδεχομένως να αλλάξουμε την επόμενη πορεία του κύκλου ζωής, δηλαδή να αλλάξουμε το πρότυπο που θα αποδοθεί και επίσης τη μέθοδο `render()` που θα κληθεί, χρησιμοποιώντας το `setView('otherView')`. -Οι παράμετροι από το αίτημα περνούν στη μέθοδο. Είναι δυνατόν και συνιστάται να καθορίσετε τύπους για τις παραμέτρους, π.χ. `actionShow(int $id, string $slug = null)` - αν η παράμετρος `id` λείπει ή αν δεν είναι ακέραιος αριθμός, ο παρουσιαστής επιστρέφει [σφάλμα 404 |#Error 404 etc.] και τερματίζει τη λειτουργία. +Οι παράμετροι από το αίτημα περνούν στη μέθοδο. Είναι δυνατόν και συνιστάται να καθορίσετε τύπους για τις παραμέτρους, π.χ. `actionShow(int $id, ?string $slug = null)` - αν η παράμετρος `id` λείπει ή αν δεν είναι ακέραιος αριθμός, ο παρουσιαστής επιστρέφει [σφάλμα 404 |#Error 404 etc.] και τερματίζει τη λειτουργία. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ $this->redirect(/* ... */); Σφάλμα 404 κ.λπ. .[#toc-error-404-etc] ====================================== -Όταν δεν μπορούμε να ικανοποιήσουμε το αίτημα επειδή για παράδειγμα το άρθρο που θέλουμε να εμφανίσουμε δεν υπάρχει στη βάση δεδομένων, θα πετάξουμε το σφάλμα 404 χρησιμοποιώντας τη μέθοδο `error(string $message = null, int $httpCode = 404)`, η οποία αντιπροσωπεύει το σφάλμα HTTP 404: +Όταν δεν μπορούμε να ικανοποιήσουμε το αίτημα επειδή για παράδειγμα το άρθρο που θέλουμε να εμφανίσουμε δεν υπάρχει στη βάση δεδομένων, θα πετάξουμε το σφάλμα 404 χρησιμοποιώντας τη μέθοδο `error(?string $message = null, int $httpCode = 404)`, η οποία αντιπροσωπεύει το σφάλμα HTTP 404: ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Παράμετροι αίτησης .[#toc-request-parameters] +============================================= + +Ο παρουσιαστής, όπως και κάθε στοιχείο, λαμβάνει τις παραμέτρους του από την αίτηση HTTP. Οι τιμές τους μπορούν να ανακτηθούν χρησιμοποιώντας τη μέθοδο `getParameter($name)` ή `getParameters()`. Οι τιμές είναι συμβολοσειρές ή πίνακες συμβολοσειρών, ουσιαστικά ακατέργαστα δεδομένα που λαμβάνονται απευθείας από τη διεύθυνση URL. + +Για μεγαλύτερη ευκολία, συνιστούμε να κάνετε τις παραμέτρους προσβάσιμες μέσω ιδιοτήτων. Απλά σχολιάστε τις με την εντολή `#[Parameter]` χαρακτηριστικό: + +```php +use Nette\Application\Attributes\Parameter; // αυτή η γραμμή είναι σημαντική + +class HomePresenter extends Nette\Application\UI\Presenter +{ + #[Parameter] + public string $theme; // πρέπει να είναι δημόσια +} +``` + +Για τις ιδιότητες, προτείνουμε να προσδιορίσετε τον τύπο δεδομένων (π.χ. `string`). Στη συνέχεια, η Nette θα μετατρέψει αυτόματα την τιμή με βάση αυτόν. Οι τιμές των παραμέτρων μπορούν επίσης να [επικυρωθούν |#Validation of Parameters]. + +Κατά τη δημιουργία ενός συνδέσμου, μπορείτε να ορίσετε απευθείας την τιμή για τις παραμέτρους: + +```latte +click +``` + + Εμμένουσες παράμετροι .[#toc-persistent-parameters] =================================================== @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Εάν το `$this->lang` έχει μια τιμή όπως `'en'`, τότε οι σύνδεσμοι που δημιουργούνται με χρήση των `link()` ή `n:href` θα περιέχουν επίσης την παράμετρο `lang=en`. Και όταν ο σύνδεσμος πατηθεί, θα είναι και πάλι `$this->lang = 'en'`. -Για τις ιδιότητες, συνιστούμε να περιλαμβάνετε τον τύπο δεδομένων (π.χ. `string`) και μπορείτε επίσης να συμπεριλάβετε μια προεπιλεγμένη τιμή. Οι τιμές των παραμέτρων μπορούν να [επικυρωθούν |#Validation of Persistent Parameters]. +Για τις ιδιότητες, συνιστούμε να συμπεριλάβετε τον τύπο δεδομένων (π.χ. `string`) και μπορείτε επίσης να συμπεριλάβετε μια προεπιλεγμένη τιμή. Οι τιμές των παραμέτρων μπορούν να [επικυρωθούν |#Validation of Parameters]. Οι μόνιμες παράμετροι μεταβιβάζονται μεταξύ όλων των ενεργειών ενός συγκεκριμένου παρουσιαστή από προεπιλογή. Για να τις περάσετε μεταξύ πολλαπλών παρουσιαστών, πρέπει να τις ορίσετε είτε: @@ -307,18 +333,12 @@ class ProductPresenter extends Nette\Application\UI\Presenter Όσα έχουμε δείξει μέχρι στιγμής σε αυτό το κεφάλαιο μάλλον αρκούν. Οι επόμενες γραμμές απευθύνονται σε όσους ενδιαφέρονται για τους παρουσιαστές σε βάθος και θέλουν να μάθουν τα πάντα. -Απαίτηση και παράμετροι .[#toc-requirement-and-parameters] ----------------------------------------------------------- +Επικύρωση των παραμέτρων .[#toc-validation-of-parameters] +--------------------------------------------------------- -Το αίτημα που χειρίζεται ο παρουσιαστής είναι το αντικείμενο [api:Nette\Application\Request] και επιστρέφεται από τη μέθοδο του παρουσιαστή `getRequest()`. Περιλαμβάνει έναν πίνακα παραμέτρων και κάθε μία από αυτές ανήκει είτε σε κάποιο από τα συστατικά είτε απευθείας στον παρουσιαστή (ο οποίος στην πραγματικότητα είναι επίσης ένα συστατικό, αν και ειδικό). Έτσι, η Nette ανακατανέμει τις παραμέτρους και περνάει μεταξύ των επιμέρους συστατικών (και του παρουσιαστή) καλώντας τη μέθοδο `loadState(array $params)`. Οι παράμετροι μπορούν να ληφθούν με τη μέθοδο `getParameters(): array`, μεμονωμένα με τη χρήση του `getParameter($name)`. Οι τιμές των παραμέτρων είναι συμβολοσειρές ή πίνακες συμβολοσειρών, είναι ουσιαστικά ακατέργαστα δεδομένα που λαμβάνονται απευθείας από μια διεύθυνση URL. +Οι τιμές των [παραμέτρων αίτησης |#request parameters] και των [μόνιμων παραμέτρων |#persistent parameters] που λαμβάνονται από τις διευθύνσεις URL εγγράφονται στις ιδιότητες από τη μέθοδο `loadState()`. Ελέγχει επίσης αν ο τύπος δεδομένων που καθορίζεται στην ιδιότητα ταιριάζει, διαφορετικά θα απαντήσει με σφάλμα 404 και η σελίδα δεν θα εμφανιστεί. - -Επικύρωση μόνιμων παραμέτρων .[#toc-validation-of-persistent-parameters] ------------------------------------------------------------------------- - -Οι τιμές των [μόνιμων παραμέτρων |#persistent parameters] που λαμβάνονται από τις διευθύνσεις URL εγγράφονται στις ιδιότητες με τη μέθοδο `loadState()`. Ελέγχει επίσης αν ο τύπος δεδομένων που καθορίζεται στην ιδιότητα ταιριάζει, διαφορετικά θα απαντήσει με σφάλμα 404 και η σελίδα δεν θα εμφανιστεί. - -Ποτέ μην εμπιστεύεστε τυφλά τις μόνιμες παραμέτρους, καθώς μπορούν εύκολα να αντικατασταθούν από τον χρήστη στη διεύθυνση URL. Για παράδειγμα, με αυτόν τον τρόπο ελέγχουμε αν το `$this->lang` είναι μεταξύ των υποστηριζόμενων γλωσσών. Ένας καλός τρόπος για να το κάνετε αυτό είναι να παρακάμψετε τη μέθοδο `loadState()` που αναφέρθηκε παραπάνω: +Ποτέ μην εμπιστεύεστε τυφλά τις παραμέτρους, καθώς μπορούν εύκολα να αντικατασταθούν από τον χρήστη στη διεύθυνση URL. Για παράδειγμα, με αυτόν τον τρόπο ελέγχουμε αν το `$this->lang` είναι μεταξύ των υποστηριζόμενων γλωσσών. Ένας καλός τρόπος για να το κάνετε αυτό είναι να παρακάμψετε τη μέθοδο `loadState()` που αναφέρθηκε παραπάνω: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,7 +361,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Αποθήκευση και επαναφορά της αίτησης .[#toc-save-and-restore-the-request] ------------------------------------------------------------------------- -Μπορείτε να αποθηκεύσετε την τρέχουσα αίτηση σε μια συνεδρία ή να την επαναφέρετε από τη συνεδρία και να αφήσετε τον παρουσιαστή να την εκτελέσει ξανά. Αυτό είναι χρήσιμο, για παράδειγμα, όταν ένας χρήστης συμπληρώνει μια φόρμα και λήγει η σύνδεσή του. Για να μην χαθούν δεδομένα, πριν από την ανακατεύθυνση στη σελίδα σύνδεσης, αποθηκεύουμε το τρέχον αίτημα στη σύνοδο χρησιμοποιώντας το `$reqId = $this->storeRequest()`, το οποίο επιστρέφει ένα αναγνωριστικό με τη μορφή σύντομης συμβολοσειράς και το περνάει ως παράμετρο στον παρουσιαστή σύνδεσης. +Το αίτημα που χειρίζεται ο παρουσιαστής είναι ένα αντικείμενο [api:Nette\Application\Request] και επιστρέφεται από τη μέθοδο του παρουσιαστή `getRequest()`. + +Μπορείτε να αποθηκεύσετε την τρέχουσα αίτηση σε μια συνεδρία ή να την επαναφέρετε από τη συνεδρία και να αφήσετε τον παρουσιαστή να την εκτελέσει ξανά. Αυτό είναι χρήσιμο, για παράδειγμα, όταν ένας χρήστης συμπληρώνει μια φόρμα και η σύνδεσή του λήγει. Για να μην χαθούν δεδομένα, πριν από την ανακατεύθυνση στη σελίδα σύνδεσης, αποθηκεύουμε την τρέχουσα αίτηση στη σύνοδο χρησιμοποιώντας τη μέθοδο `$reqId = $this->storeRequest()`, η οποία επιστρέφει ένα αναγνωριστικό με τη μορφή σύντομης συμβολοσειράς και το περνάει ως παράμετρο στον παρουσιαστή σύνδεσης. Μετά την είσοδο, καλούμε τη μέθοδο `$this->restoreRequest($reqId)`, η οποία παραλαμβάνει το αίτημα από τη σύνοδο και το προωθεί σε αυτήν. Η μέθοδος επαληθεύει ότι το αίτημα δημιουργήθηκε από τον ίδιο χρήστη που τώρα έχει συνδεθεί είναι. Αν συνδεθεί άλλος χρήστης ή το κλειδί είναι άκυρο, δεν κάνει τίποτα και το πρόγραμμα συνεχίζει. @@ -362,7 +384,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Μπορείτε επίσης να επικαλεστείτε την κανονικοποίηση χειροκίνητα χρησιμοποιώντας τη μέθοδο `canonicalize()`, η οποία, όπως και η μέθοδος `link()`, λαμβάνει τον παρουσιαστή, τις ενέργειες και τις παραμέτρους ως ορίσματα. Δημιουργεί έναν σύνδεσμο και τον συγκρίνει με την τρέχουσα διεύθυνση URL. Εάν είναι διαφορετική, ανακατευθύνει στον δημιουργημένο σύνδεσμο. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // ανακατευθύνει εάν το $slug είναι διαφορετικό από το $realSlug @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Περιορισμός πρόσβασης με χρήση `#[Requires]` .[#toc-access-restriction-using-requires]{data-version:3.2.2} +---------------------------------------------------------------------------------------------------------- + +Το `#[Requires]` παρέχει προηγμένες επιλογές για τον περιορισμό της πρόσβασης στους παρουσιαστές και τις μεθόδους τους. Μπορεί να χρησιμοποιηθεί για τον προσδιορισμό μεθόδων HTTP, την απαίτηση αιτήσεων AJAX, τον περιορισμό της πρόσβασης στην ίδια προέλευση και τον περιορισμό της πρόσβασης μόνο στην προώθηση. Το χαρακτηριστικό μπορεί να εφαρμοστεί σε κλάσεις παρουσιαστών καθώς και σε μεμονωμένες μεθόδους όπως οι `action()`, `render()`, `handle()`, και `createComponent()`. + +Μπορείτε να καθορίσετε αυτούς τους περιορισμούς: +- σε μεθόδους HTTP: `#[Requires(methods: ['GET', 'POST'])]` +- AJAX: `#[Requires(ajax: true)]` +- πρόσβαση μόνο από την ίδια προέλευση: `#[Requires(sameOrigin: true)]` +- πρόσβαση μόνο μέσω προώθησης: `#[Requires(forward: true)]` +- περιορισμοί σε συγκεκριμένες ενέργειες: `#[Requires(actions: 'default')]` + +Για λεπτομέρειες, ανατρέξτε στην ενότητα [Πώς να χρησιμοποιήσετε το Requires attribute |best-practices:attribute-requires]. + + +Έλεγχος μεθόδου HTTP .[#toc-http-method-check] +---------------------------------------------- + +Στη 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). Εάν επιτρέψετε αυτή τη μέθοδο αλλά δεν υλοποιήσετε μια κατάλληλη απάντηση, μπορεί να οδηγήσει σε ασυνέπειες και πιθανά ζητήματα ασφάλειας. + + Περαιτέρω ανάγνωση .[#toc-further-reading] ========================================== diff --git a/application/el/routing.texy b/application/el/routing.texy index 448fa6f67b..0ebcc505ba 100644 --- a/application/el/routing.texy +++ b/application/el/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Ή μπορούμε να χρησιμοποιήσουμε αυτή τη μορφή, παρατηρήστε την αναδιατύπωση της κανονικής έκφρασης επικύρωσης: +Για μια πιο λεπτομερή προδιαγραφή, μπορεί να χρησιμοποιηθεί μια ακόμη πιο εκτεταμένη μορφή, όπου εκτός από τις προεπιλεγμένες τιμές, μπορούν να οριστούν και άλλες ιδιότητες παραμέτρων, όπως μια κανονική έκφραση επικύρωσης (βλ. την παράμετρο `id` ): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Αυτές οι πιο ομιλητικές μορφές είναι χρήσιμες για την προσθήκη άλλων μεταδεδομένων. +Είναι σημαντικό να σημειωθεί ότι εάν οι παράμετροι που ορίζονται στον πίνακα δεν περιλαμβάνονται στη μάσκα διαδρομής, οι τιμές τους δεν μπορούν να αλλάξουν, ούτε καν με τη χρήση παραμέτρων ερωτήματος που καθορίζονται μετά από ένα ερωτηματικό στη διεύθυνση URL. Φίλτρα και μεταφράσεις .[#toc-filters-and-translations] @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Ενότητες .[#toc-modules] ------------------------ -Αν έχουμε περισσότερες διαδρομές που ανήκουν σε ένα [module |modules], μπορούμε να χρησιμοποιήσουμε το `withModule()` για να τις ομαδοποιήσουμε: +Αν έχουμε περισσότερες διαδρομές που ανήκουν σε μία [ενότητα |directory-structure#Presenters and Templates], μπορούμε να χρησιμοποιήσουμε το `withModule()` για να τις ομαδοποιήσουμε: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Ενσωμάτωση .[#toc-integration] ============================== -Για να συνδέσουμε τον δρομολογητή μας στην εφαρμογή, πρέπει να ενημερώσουμε το DI container σχετικά με αυτόν. Ο ευκολότερος τρόπος είναι να προετοιμάσουμε το εργοστάσιο που θα κατασκευάσει το αντικείμενο του δρομολογητή και να πούμε στη διαμόρφωση του δοχείου να το χρησιμοποιήσει. Ας πούμε λοιπόν ότι γράφουμε μια μέθοδο για το σκοπό αυτό `App\Router\RouterFactory::createRouter()`: +Για να συνδέσουμε τον δρομολογητή μας στην εφαρμογή, πρέπει να ενημερώσουμε το DI container σχετικά με αυτόν. Ο ευκολότερος τρόπος είναι να προετοιμάσουμε το εργοστάσιο που θα κατασκευάσει το αντικείμενο του δρομολογητή και να πούμε στη διαμόρφωση του δοχείου να το χρησιμοποιήσει. Ας πούμε λοιπόν ότι γράφουμε μια μέθοδο για το σκοπό αυτό `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ class RouterFactory ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Οποιεσδήποτε εξαρτήσεις, όπως μια σύνδεση βάσης δεδομένων κ.λπ., περνούν στη μέθοδο factory ως παράμετροι με τη χρήση [αυτόματης σύνδεσης |dependency-injection:autowiring]: @@ -663,7 +663,7 @@ $router->addRoute(/* ... */); Έτσι και πάλι θα δημιουργήσουμε μια μέθοδο που θα κατασκευάσει ένα δρομολογητή, για παράδειγμα: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Ή θα δημιουργήσουμε αντικείμενα απευθείας: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/el/templates.texy b/application/el/templates.texy index 4b83599f45..a26b5532fb 100644 --- a/application/el/templates.texy +++ b/application/el/templates.texy @@ -34,35 +34,81 @@ Ορίζει το μπλοκ `content`, το οποίο εισάγεται στη θέση του `{include content}` στη διάταξη, και επίσης επαναπροσδιορίζει το μπλοκ `title`, το οποίο αντικαθιστά το `{block title}` στη διάταξη. Προσπαθήστε να φανταστείτε το αποτέλεσμα. -Αναζήτηση προτύπων .[#toc-search-for-templates] ------------------------------------------------ +Αναζήτηση προτύπου .[#toc-template-lookup] +------------------------------------------ -Η διαδρομή προς τα πρότυπα προκύπτει σύμφωνα με μια απλή λογική. Προσπαθεί να δει αν ένα από αυτά τα αρχεία προτύπων υπάρχει σε σχέση με τον κατάλογο όπου βρίσκεται η κλάση presenter, όπου `` είναι το όνομα του τρέχοντος παρουσιαστή και `` είναι το όνομα της τρέχουσας δράσης: +Στους παρουσιαστές, δεν χρειάζεται να καθορίσετε ποιο πρότυπο πρέπει να αποδοθεί- το πλαίσιο θα καθορίσει αυτόματα τη διαδρομή, διευκολύνοντας την κωδικοποίηση για εσάς. -- `templates//.latte` -- `templates/..latte` +Αν χρησιμοποιείτε μια δομή καταλόγου όπου κάθε παρουσιαστής έχει το δικό του κατάλογο, απλά τοποθετήστε το πρότυπο σε αυτόν τον κατάλογο κάτω από το όνομα της ενέργειας (π.χ. προβολή). Για παράδειγμα, για τη δράση `default`, χρησιμοποιήστε το πρότυπο `default.latte`: -Αν το πρότυπο δεν βρεθεί, θα προσπαθήσει να ψάξει στον κατάλογο `templates` ένα επίπεδο πιο πάνω, δηλαδή στο ίδιο επίπεδο με τον κατάλογο με την κλάση παρουσιαστή. +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -Εάν το πρότυπο δεν βρεθεί ούτε εκεί, η απάντηση είναι ένα [σφάλμα 404 |presenters#Error 404 etc.]. +Εάν χρησιμοποιείτε μια δομή όπου οι παρουσιαστές βρίσκονται μαζί σε έναν κατάλογο και τα πρότυπα σε έναν φάκελο `templates`, αποθηκεύστε το είτε σε ένα αρχείο `..latte` είτε στο `/.latte`: -Μπορείτε επίσης να αλλάξετε την προβολή χρησιμοποιώντας το `$this->setView('otherView')`. Ή, αντί για αναζήτηση, καθορίστε απευθείας το όνομα του αρχείου προτύπου χρησιμοποιώντας τη διεύθυνση `$this->template->setFile('/path/to/template.latte')`. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- + +Ο κατάλογος `templates` μπορεί επίσης να τοποθετηθεί ένα επίπεδο ψηλότερα, στο ίδιο επίπεδο με τον κατάλογο με τις κλάσεις παρουσιαστών. + +Εάν το πρότυπο δεν βρεθεί, ο παρουσιαστής απαντά με το [σφάλμα 404 - σελίδα δεν βρέθηκε |presenters#Error 404 etc]. + +Μπορείτε να αλλάξετε την προβολή χρησιμοποιώντας το `$this->setView('anotherView')`. Είναι επίσης δυνατό να καθορίσετε απευθείας το αρχείο προτύπου με το `$this->template->setFile('/path/to/template.latte')`. .[note] -Μπορείτε να αλλάξετε τις διαδρομές στις οποίες αναζητούνται τα πρότυπα υπερκαλύπτοντας τη μέθοδο [formatTemplateFiles |api:Nette\Application\UI\Presenter::formatTemplateFiles()], η οποία επιστρέφει έναν πίνακα πιθανών διαδρομών αρχείων. +Τα αρχεία στα οποία αναζητούνται τα πρότυπα μπορούν να αλλάξουν με την παράκαμψη της μεθόδου [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], η οποία επιστρέφει έναν πίνακα πιθανών ονομάτων αρχείων. + + +Αναζήτηση προτύπων διάταξης .[#toc-layout-template-lookup] +---------------------------------------------------------- + +Η Nette αναζητά επίσης αυτόματα το αρχείο διάταξης. + +Εάν χρησιμοποιείτε μια δομή καταλόγου όπου κάθε παρουσιαστής έχει το δικό του κατάλογο, τοποθετήστε τη διάταξη είτε στο φάκελο με τον παρουσιαστή, εάν αφορά μόνο αυτόν, είτε ένα επίπεδο ψηλότερα εάν είναι κοινή για πολλούς παρουσιαστές: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +Εάν χρησιμοποιείτε μια δομή όπου οι παρουσιαστές είναι ομαδοποιημένοι σε έναν κατάλογο και τα πρότυπα βρίσκονται σε έναν φάκελο `templates`, η διάταξη αναμένεται στις ακόλουθες θέσεις: -Η διάταξη αναμένεται στα ακόλουθα αρχεία: +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` διάταξη κοινή για πολλούς παρουσιαστές +Εάν ο παρουσιαστής βρίσκεται σε μια ενότητα, θα αναζητήσει επίσης πιο πάνω στο δέντρο καταλόγων σύμφωνα με την ένθεση της ενότητας. -`` είναι το όνομα του τρέχοντος παρουσιαστή και `` είναι το όνομα της διάταξης, η οποία είναι εξ ορισμού `'layout'`. Το όνομα μπορεί να αλλάξει με το `$this->setLayout('otherLayout')`, έτσι ώστε να δοκιμάζονται τα αρχεία `@otherLayout.latte`. +Το όνομα της διάταξης μπορεί να αλλάξει χρησιμοποιώντας το `$this->setLayout('layoutAdmin')` και τότε θα αναμένεται στο αρχείο `@layoutAdmin.latte`. Μπορείτε επίσης να καθορίσετε απευθείας το αρχείο προτύπου διάταξης χρησιμοποιώντας το `$this->setLayout('/path/to/template.latte')`. -Μπορείτε επίσης να καθορίσετε απευθείας το όνομα του αρχείου του προτύπου διάταξης χρησιμοποιώντας το `$this->setLayout('/path/to/template.latte')`. Η χρήση του `$this->setLayout(false)` θα απενεργοποιήσει την αναζήτηση διάταξης. +Η χρήση του `$this->setLayout(false)` ή της ετικέτας `{layout none}` μέσα στο πρότυπο απενεργοποιεί την αναζήτηση διάταξης. .[note] -Μπορείτε να αλλάξετε τις διαδρομές στις οποίες αναζητούνται τα πρότυπα με την παράκαμψη της μεθόδου [formatLayoutTemplateFiles |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], η οποία επιστρέφει έναν πίνακα πιθανών διαδρομών αρχείων. +Τα αρχεία στα οποία αναζητούνται τα πρότυπα διάταξης μπορούν να αλλάξουν με την παράκαμψη της μεθόδου [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], η οποία επιστρέφει έναν πίνακα πιθανών ονομάτων αρχείων. Μεταβλητές στο πρότυπο .[#toc-variables-in-the-template] @@ -104,7 +150,7 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template Μπορείτε επίσης να αφεθείτε στην πολυτέλεια του ψιθυρίσματος στα πρότυπα, απλά εγκαταστήστε το πρόσθετο Latte στο PhpStorm και καθορίστε το όνομα της κλάσης στην αρχή του προτύπου, δείτε το άρθρο "Latte: πώς να πληκτρολογήσετε το σύστημα":https://blog.nette.org/el/latte-pos-na-chresimopoiesete-to-systema-typon: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void Latte έκδοση 3 προσφέρει έναν πιο προηγμένο τρόπο δημιουργώντας μια [επέκταση |latte:creating-extension] για κάθε έργο ιστού. Εδώ είναι ένα πρόχειρο παράδειγμα μιας τέτοιας κλάσης: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ final class LatteExtension extends Latte\Extension ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ protected function beforeRender(): void ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` Ο μεταφραστής μπορεί στη συνέχεια να χρησιμοποιηθεί, για παράδειγμα, ως φίλτρο `|translate`, με πρόσθετες παραμέτρους που περνούν στη μέθοδο `translate()` (βλ. `foo, bar`): diff --git a/application/en/@home.texy b/application/en/@home.texy index fac8c0d767..69bea8c6ba 100644 --- a/application/en/@home.texy +++ b/application/en/@home.texy @@ -2,35 +2,84 @@ Nette Application ***************** .[perex] -The `nette/application` package is the basis for creating interactive web applications. - -- [How do applications work? |how-it-works] -- [Bootstrap] -- [Presenters] -- [Templates] -- [Modules] -- [Routing] -- [Creating URL Links |creating-links] -- [Interactive Components |components] -- [AJAX & Snippets |ajax] -- [Multiplier |multiplier] -- [Configuration] +Nette Application is the core of the Nette framework that brings powerful tools for creating modern web applications. It offers numerous exceptional features that significantly simplify development and improve code security and maintainability. Installation ------------ -Download and install the package using [Composer|best-practices:composer]: +Download and install the library using [Composer|best-practices:composer]: ```shell composer require nette/application ``` -| version | compatible with PHP + +Why choose Nette Application? +----------------------------- + +Nette has always been a pioneer in web technologies. + +**Bidirectional Router:** Nette features an advanced routing system unique in its bidirectionality - it not only translates URLs to application actions but can also generate URLs in reverse. This means: +- You can modify the URL structure of the entire application at any time without modifying template files +- URLs are automatically canonicalized, improving SEO +- Routing is defined in one place, not scattered in annotations + +**Components and Signals:** The built-in component system inspired by Delphi and React.js is unique among PHP frameworks: +- Enables creating reusable UI elements +- Supports hierarchical component composition +- Offers elegant AJAX request handling using signals +- Rich library of ready-made components on [Componette](https://componette.org) + +**AJAX and Snippets:** Nette introduced a revolutionary way of working with AJAX in 2009, before solutions like Hotwire for Ruby on Rails or Symfony UX Turbo: +- Snippets allow updating only parts of the page without writing JavaScript +- Automatic integration with the component system +- Smart invalidation of page sections +- Minimal data transfer + +**Intuitive [Latte|latte:] Templates:** The most secure templating system for PHP with advanced features: +- Automatic XSS protection with context-sensitive escaping +- Extensible with custom filters, functions, and tags +- Template inheritance and snippets for AJAX +- Excellent PHP 8.x support with type system + +**Dependency Injection:** Nette fully utilizes Dependency Injection: +- Automatic dependency passing (autowiring) +- Configuration using clear NEON format +- Support for component factories + + +Main Benefits +------------- + +- **Security**: Automatic protection against [vulnerabilities|nette:vulnerability-protection] like XSS, CSRF, etc. +- **Productivity**: Less writing, more features thanks to smart design +- **Debugging**: [Tracy debugger|tracy:] with routing panel +- **Performance**: Intelligent caching system, lazy loading of components +- **Flexibility**: Easy URL modification even after application completion +- **Components**: Unique system of reusable UI elements +- **Modern**: Full support for PHP 8.4+ and type system + + +Getting Started +--------------- + +1. [Understanding Applications |how-it-works] - Understanding basic architecture +2. [Presenters |presenters] - Working with presenters and actions +3. [Templates |templates] - Creating templates in Latte +4. [Routing |routing] - URL configuration +5. [Interactive Components |components] - Using the component system + + +PHP Compatibility +----------------- + +| version | compatible with PHP |-----------|------------------- -| Nette Application 4.0 | PHP 8.0 – 8.2 -| Nette Application 3.1 | PHP 7.2 – 8.2 +| Nette Application 4.0 | PHP 8.1 – 8.4 +| Nette Application 3.2 | PHP 8.1 – 8.4 +| Nette Application 3.1 | PHP 7.2 – 8.3 | Nette Application 3.0 | PHP 7.1 – 8.0 | Nette Application 2.4 | PHP 5.6 – 8.0 -Applies to the latest patch versions. +Valid for the latest patch versions. \ No newline at end of file diff --git a/application/en/@left-menu.texy b/application/en/@left-menu.texy index bc4b360cee..94208d2550 100644 --- a/application/en/@left-menu.texy +++ b/application/en/@left-menu.texy @@ -4,7 +4,7 @@ Nette Application - [Bootstrap] - [Presenters] - [Templates] -- [Modules] +- [Directory structure |directory-structure] - [Routing] - [Creating URL Links |creating-links] - [Interactive Components |components] diff --git a/application/en/ajax.texy b/application/en/ajax.texy index f43ff28c2f..819a6e4627 100644 --- a/application/en/ajax.texy +++ b/application/en/ajax.texy @@ -3,10 +3,10 @@ AJAX & Snippets
    -Modern web applications nowadays run half on a server and half in a browser. AJAX is a vital uniting factor. What support does the Nette Framework offer? -- sending template fragments (so-called *snippets*) +In the era of modern web applications, where functionality often spans between the server and the browser, AJAX is an essential connecting element. What options does the Nette Framework offer in this area? +- sending parts of the template, so-called snippets - passing variables between PHP and JavaScript -- AJAX applications debugging +- tools for debugging AJAX requests
    @@ -14,29 +14,32 @@ Modern web applications nowadays run half on a server and half in a browser. AJA AJAX Request ============ -An AJAX request does not differ from a classic request - the presenter is called with a specific view and parameters. It is also up to the presenter how to respond to it: it can use its own routine, which returns an HTML code fragment (HTML snippet), an XML document, a JSON object, or JavaScript code. +An AJAX request fundamentally does not differ from a classic HTTP request. A presenter is called with specific parameters. It's up to the presenter how to respond to the request - it can return data in JSON format, send a part of HTML code, an XML document, etc. -On the server side, an AJAX request can be detected using the service method [encapsulating the HTTP request |http:request] `$httpRequest->isAjax()` (detects based on the HTTP header `X-Requested-With`). Inside the presenter, a shortcut is available in the form of the method `$this->isAjax()`. +On the browser side, we initiate an AJAX request using the `fetch()` function: -There is a pre-processed object called `payload` dedicated to sending data to the browser in JSON. - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // processing the response +}); ``` -For a full control over your JSON output use the `sendJson` method in your presenter. It terminates presenter immediately and you'll do without a template: +On the server side, an AJAX request is recognized by the `$httpRequest->isAjax()` method of the service [encapsulating the HTTP request |http:request]. It uses the HTTP header `X-Requested-With`, so it's essential to send it. Within the presenter, you can use the `$this->isAjax()` method. + +If you want to send data in JSON format, use the [`sendJson()` |presenters#Sending a response] method. The method also terminates the presenter's activity. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -If we want to send HTML, we can either set a special template for AJAX requests: +If you plan to respond with a special template designed for AJAX, you can do it as follows: ```php public function handleClick($param): void @@ -49,22 +52,38 @@ public function handleClick($param): void ``` +Snippets +======== + +The most powerful tool offered by Nette for connecting the server with the client are snippets. With them, you can turn an ordinary application into an AJAX one with minimal effort and a few lines of code. The Fifteen example demonstrates how it all works, and its code can be found on [GitHub |https://github.com/nette-examples/fifteen]. + +Snippets, or clippings, allow you to update only parts of the page, instead of reloading the entire page. This is faster and more efficient, and also provides a more comfortable user experience. Snippets might remind you of Hotwire for Ruby on Rails or Symfony UX Turbo. Interestingly, Nette introduced snippets 14 years earlier. + +How do snippets work? When the page is first loaded (a non-AJAX request), the entire page, including all snippets, is loaded. When the user interacts with the page (e.g., clicks a button, submits a form, etc.), instead of loading the entire page, an AJAX request is made. The code in the presenter performs the action and decides which snippets need updating. Nette renders these snippets and sends them in the form of a JSON array. The handling code in the browser then inserts the received snippets back into the page. Therefore, only the code of the changed snippets is transferred, saving bandwidth and speeding up loading compared to transferring the entire page content. + + Naja -==== +---- -The [Naja library|https://naja.js.org] is used to handle AJAX requests on the browser side. [Install |https://naja.js.org/#/guide/01-install-setup-naja] it as a node.js package (to use with Webpack, Rollup, Vite, Parcel and more): +To handle snippets on the browser side, the [Naja library |https://naja.js.org] is used. [Install it |https://naja.js.org/#/guide/01-install-setup-naja] as a node.js package (for use with applications such as Webpack, Rollup, Vite, Parcel, and others): ```shell npm install naja ``` -…or insert it directly into the page template: +... or insert it directly into the page template: ```html ``` -To create an AJAX request from a regular link (signal) or form submittion, simply mark the relevant link, form, or button with the class `ajax`: +First you need to [initialize |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] the library: + +```js +naja.initialize(); +``` + +To make an ordinary link (signal) or form submission an AJAX request, simply mark the respective link, form, or button with the `ajax` class: ```html Go @@ -74,64 +93,39 @@ To create an AJAX request from a regular link (signal) or form submittion, simpl or +
    ``` -Snippets -======== - -There is a far more powerful tool of built-in AJAX support – snippets. Using them makes it possible to turn a regular application into an AJAX one using only a few lines of code. How it all works is demonstrated in the Fifteen example whose code is also accessible in the build or on [GitHub |https://github.com/nette-examples/fifteen]. - -The way snippets work is that the whole page is transferred during the initial (i.e. non-AJAX) request and then with every AJAX [subrequest |components#signal] (request of the same view of the same presenter) only the code of the changed parts is transferred in the `payload` repository mentioned earlier. +Redrawing Snippets +------------------ -Snippets may remind you of Hotwire for Ruby on Rails or Symfony UX Turbo, but Nette came up with them fourteen years earlier. - - -Invalidation of Snippets -======================== - -Each descendant of class [Control |components] (which a Presenter is too) is able to remember whether there were any changes during a request that require it to re-render. There are a pair of methods to handle this: `redrawControl()` and `isControlInvalid()`. An example: +Every object of the [Control |components] class (including the Presenter itself) keeps a record of whether changes have occurred that necessitate its redrawing. The `redrawControl()` method is employed for this purpose. ```php public function handleLogin(string $user): void { - // The object has to re-render after the user has logged in + // after logging in, it is necessary to redraw the relevant part $this->redrawControl(); // ... } ``` -Nette however offers an even finer resolution than whole components. The listed methods accept the name of a so-called "snippet" as an optional parameter. A "snippet" is basically an element in your template marked for that purpose by a Latte tag, more on that later. Thus it is possible to ask a component to redraw only *parts* of its template. If the entire component is invalidated then all of its snippets are re-rendered. A component is “invalid” also if any of its subcomponents is invalid. - -```php -$this->isControlInvalid(); // -> false -$this->redrawControl('header'); // invalidates the snippet named 'header' -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, at least one snippet is invalid +Nette also allows for finer control of what needs redrawing. The aforementioned method can take the snippet name as an argument. Thus, it's possible to invalidate (meaning: force a redraw) at the template part level. If the entire component is invalidated, every snippet of it is also redrawn: -$this->redrawControl(); // invalidates the whole component, every snippet -$this->isControlInvalid('footer'); // -> true +```php +// invalidates the 'header' snippet +$this->redrawControl('header'); ``` -A component which receives a signal is automatically marked for redrawing. - -Thanks to snippet redrawing we know exactly which parts of which elements should be re-rendered. - - -Tag `{snippet} … {/snippet}` .{toc: Tag snippet} -================================================ -Rendering of the page proceeds very similarly to a regular request: the same templates are loaded, etc. The vital part is, however, to leave out the parts that are not supposed to reach the output; the other parts shall be associated with an identifier and sent to the user in a comprehensible format for a JavaScript handler. +Snippets in Latte +----------------- - -Syntax ------- - -If there is a control or a snippet in the template, we have to wrap it using the `{snippet} ... {/snippet}` pair tag - it will make sure that the rendered snippet will be "cut out" and sent to the browser. It will also enclose it in a helper `
    ` tag (it is possible to use a different one). In the following example a snippet named `header` is defined. It may as well represent the template of a component: +Using snippets in Latte is extremely easy. To define a part of the template as a snippet, simply wrap it in `{snippet}` and `{/snippet}` tags: ```latte {snippet header} @@ -139,7 +133,9 @@ If there is a control or a snippet in the template, we have to wrap it using the {/snippet} ``` -A snippet of a type other than `
    ` or a snippet with additional HTML attributes is achieved by using the attribute variant: +The snippet creates an element `
    ` in the HTML page with a specially generated `id`. When redrawing a snippet, the content of this element is updated. Therefore, when the page is initially rendered, all snippets must also be rendered, even if they may initially be empty. + +You can also create a snippet with an element other than `
    ` using an n:attribute: ```latte
    @@ -148,138 +144,106 @@ A snippet of a type other than `
    ` or a snippet with additional HTML attribu ``` -Dynamic Snippets -================ +Snippet Areas +------------- -In Nette you can also define snippets with a dynamic name based on a runtime parameter. This is most suitable for various lists where we need to change just one row but we don't want transfer the whole list along with it. An example of this would be: +Snippet names can also be expressions: ```latte -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -There is one static snippet called `itemsContainer`, containing several dynamic snippets: `item-0`, `item-1` and so on. +This way, we'll get several snippets like `item-0`, `item-1`, etc. If we were to directly invalidate a dynamic snippet (e.g., `item-1`), nothing would be redrawn. The reason being, snippets function as true excerpts and only they themselves are rendered directly. However, in the template, there isn't technically a snippet named `item-1`. It only emerges when executing the surrounding code of the snippet, in this case, the foreach loop. Hence, we'll mark the part of the template that needs to be executed with the `{snippetArea}` tag: -You can't redraw a dynamic snippet directly (redrawing of `item-1` has no effect), you have to redraw its parent snippet (in this example `itemsContainer`). This causes the code of the parent snippet to be executed, but then just its sub-snippets are sent to the browser. If you want to send over just one of the sub-snippets, you have to modify input for the parent snippet to not generate the other sub-snippets. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -In the example above you have to make sure that for an AJAX request only one item will be added to the `$list` array, therefore the `foreach` loop will print just one dynamic snippet. +And we'll redraw both the individual snippet and the entire overarching area: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * This method returns data for the list. - * Usually this would just request the data from a model. - * For the purpose of this example, the data is hard-coded. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +It's also essential to ensure that the `$items` array contains only the items that should be redrawn. -Snippets in an Included Template -================================ - -It can happen that the snippet is in a template which is being included from a different template. In that case we need to wrap the inclusion code in the second template with the `snippetArea` tag, then we redraw both the snippetArea and the actual snippet. - -Tag `snippetArea` ensures that the code inside is executed but only the actual snippet in the included template is sent to the browser. +When inserting another template into the main one using the `{include}` tag, which has snippets, it's necessary to again wrap the included template in a `snippetArea` and invalidate both the snippet and the area together: ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* included.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -You can also combine it with dynamic snippets. +Snippets in Components +---------------------- -Adding and Deleting -=================== - -If you add a new item into the list and invalidate `itemsContainer`, the AJAX request returns snippets including the new one, but the javascript handler won’t be able to render it. This is because there is no HTML element with the newly created ID. - -In this case, the simplest way is to wrap the whole list in one more snippet and invalidate it all: +You can create snippets within [components], and Nette will automatically redraw them. However, there's a specific limitation: to redraw snippets, it calls the `render()` method without any parameters. Thus, passing parameters in the template won't work: ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Sending User Data +----------------- + +Along with snippets, you can send any additional data to the client. Simply write them into the `payload` object: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + // ... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -The same goes for deleting an item. It would be possible to send empty snippet, but usually lists can be paginated and it would be complicated to implement deleting one item and loading another (which used to be on a different page of the paginated list). - -Sending Parameters to Component -=============================== +Sending Parameters +================== When we send parameters to the component via AJAX request, whether signal parameters or persistent parameters, we must provide their global name, which also contains the name of the component. The full name of parameter returns the `getParameterId()` method. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -And handle method with s corresponding parameters in component. +A handle method with the corresponding parameters in the component: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/en/bootstrap.texy b/application/en/bootstrap.texy index c763e715a3..6459dee6d0 100644 --- a/application/en/bootstrap.texy +++ b/application/en/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + private Configurator $configurator; + private string $rootDir; + + public function __construct() + { + $this->rootDir = dirname(__DIR__); + // The configurator is responsible for setting up the application environment and services. + $this->configurator = new Configurator; + // Set the directory for temporary files generated by Nette (e.g. compiled templates) + $this->configurator->setTempDirectory($this->rootDir . '/temp'); + } + + public function bootWebApplication(): Nette\DI\Container + { + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); + } + + private function initializeEnvironment(): void { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // Nette is smart, and the development mode turns on automatically, + // or you can enable for a specific IP address it by uncommenting the following line: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Enables Tracy: the ultimate "swiss army knife" debugging tool. + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: autoloads all classes in the given directory + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Load configuration files + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php ========= -In the case of web applications, the initial file is `index.php`, which is located in the public directory `www/`. It lets the `Bootstrap` class to initialize the environment and return the `$configurator` which creates DI container. Then it obtains the `Application` service, that runs the web application: +In the case of web applications, the primary file is `index.php`, which is located in the [public directory |directory-structure#public-directory-www] `www/`. This will have the Bootstrap class initialize the environment and produce a DI container. It then gets the `Application` service from it, which starts the web application: ```php -// initialize the environment + get Configurator object -$configurator = App\Bootstrap::boot(); -// create a DI container -$container = $configurator->createContainer(); +$bootstrap = new App\Bootstrap; +// Initialize the environment + create a DI container +$container = $bootstrap->bootWebApplication(); // DI container creates a Nette\Application\Application object $application = $container->getByType(Nette\Application\Application::class); -// start Nette application +// Start the Nette application and handle the incoming request $application->run(); ``` @@ -59,26 +84,42 @@ As you can see, the [api:Nette\Bootstrap\Configurator] class, which we will now Development vs Production Mode ============================== -Nette distinguishes between two basic modes in which a request is executed: development and production. The development mode is focused on the maximum comfort of the programmer, Tracy is displayed, the cache is automatically updated when changing templates or DI container configuration, etc. Production mode is focused on performance, Tracy only logs errors and changes of templates and other files are not checked. +Nette behaves differently depending on whether it is running on a development or production server: + +🛠️ Development Mode: + - Displays the Tracy debug bar with useful information (e.g., SQL queries, execution time, memory usage). + - Shows a detailed error page with function call traces and variable contents when an error occurs. + - Automatically refreshes the cache when Latte templates, configuration files, etc., are modified. + + +🚀 Production Mode: + - Does not display any debugging information; all errors are logged. + - Shows an `ErrorPresenter` or a generic "Server Error" page when an error occurs. + - Cache is never automatically refreshed! + - Optimized for speed and security. -Mode selection is done by autodetection, so there is usually no need to configure or switch anything manually. The mode is development if the application is running on localhost (ie IP address `127.0.0.1` or `::1`) and no proxy is present (ie its HTTP header). Otherwise, it runs in production mode. + +The mode is determined automatically, so in most cases, there’s no need to configure or switch it manually: + +- Development mode: Active on localhost (IP address `127.0.0.1` or `::1`) unless a proxy is in use (i.e., based on its HTTP headers). +- Production mode: Active everywhere else. If you want to enable development mode in other cases, for example, for programmers accessing from a specific IP address, you can use `setDebugMode()`: ```php -$configurator->setDebugMode('23.75.345.200'); // one or more IP addresses +$this->configurator->setDebugMode('23.75.345.200'); // one or more IP addresses ``` We definitely recommend combining an IP address with a cookie. We will store a secret token into the `nette-debug` cookie, e.g. `secret1234`, and the development mode will be activated for programmers with this combination of IP and cookie. ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` We can also turn off developer mode completely, even for localhost: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` Note that the value `true` turns on developer mode by hard, which should never happen on a production server. @@ -90,7 +131,7 @@ Debugging Tool Tracy For easy debugging, we will turn on the great tool [Tracy |tracy:]. In developer mode it visualizes errors and in production mode it logs errors to the specified directory: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ Temporary Files Nette uses the cache for DI container, RobotLoader, templates, etc. Therefore it is necessary to set the path to the directory where the cache will be stored: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` On Linux or macOS, set the [write permissions |nette:troubleshooting#Setting directory permissions] for directories `log/` and `temp/`. @@ -112,7 +153,7 @@ RobotLoader Usually, we will want to automatically load the classes using [RobotLoader |robot-loader:], so we have to start it up and let it load classes from the directory where `Bootstrap.php` is located (i.e. `__DIR__`) and all its subdirectories: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ Timezone Configurator allows you to specify a timezone for your application. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ In the development mode, the container is automatically updated each time you ch Configuration files are loaded using `addConfig()`: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` The method `addConfig()` can be called multiple times to add multiple files. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ Static Parameters Parameters used in configuration files can be defined [in the section `parameters`|dependency-injection:configuration#parameters] and also passed (or overwritten) by the `addStaticParameters()` method (it has alias `addParameters()`). It is important that different parameter values cause the generation of additional DI containers, i.e. additional classes. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ Dynamic Parameters We can also add dynamic parameters to the container, their different values, unlike static parameters, will not cause the generation of new DI containers. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ Environment variables could be easily made available using dynamic parameters. We can access them via `%env.variable%` in configuration files. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ You can use the following static parameters in the configuration files: - `%wwwDir%` is the absolute path to the directory containing the `index.php` entry file - `%tempDir%` is the absolute path to the directory for temporary files - `%vendorDir%` is the absolute path to the directory where Composer installs libraries +- `%rootDir%` is the absolute path to the root directory of the project - `%debugMode%` indicates whether the application is in debug mode - `%consoleMode%` indicates whether the request came through the command line @@ -225,7 +268,7 @@ services: Create a new instance and insert it in bootstrap: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Different Environments ====================== -Feel free to customize the `Bootstrap` class to suit your needs. You can add parameters to the `boot()` method to differentiate web projects, or add other methods, such as `bootForTests()`, which initializes the environment for unit tests, `bootForCli()` for scripts called from the command line, and so on. +Don't hesitate to customize the `Bootstrap` class according to your needs. You can add parameters to the `bootWebApplication()` method to differentiate between web projects. Alternatively, you can add other methods, such as `bootTestEnvironment()` to initialize the environment for unit tests, `bootConsoleApplication()` for scripts called from the command line, and so on. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container { - $configurator = self::boot(); Tester\Environment::setup(); // Nette Tester initialization - return $configurator; + $this->setupContainer(); + return $this->configurator->createContainer(); +} + +public function bootConsoleApplication(): Nette\DI\Container +{ + $this->configurator->setDebugMode(false); + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); } ``` diff --git a/application/en/components.texy b/application/en/components.texy index b610a84739..2c79308bef 100644 --- a/application/en/components.texy +++ b/application/en/components.texy @@ -198,7 +198,7 @@ The link that calls the signal is created in the usual way, i.e. in the template click here ``` -The signal is always called on the current presenter and view, so it is not possible to link to signal in different presenter / action. +The signal is always called on the current presenter and action, it cannot be called on another presenter or action. Thus, the signal causes the page to be reloaded in exactly the same way as in the original request, only in addition it calls the signal handling method with the appropriate parameters. If the method does not exist, exception [api:Nette\Application\UI\BadSignalException] is thrown, which is displayed to the user as error page 403 Forbidden. @@ -230,6 +230,28 @@ In the template, these messages are available in the variable `$flashes` as obje ``` +Redirection After a Signal +========================== + +After processing a component signal, redirection often follows. This situation is similar to forms—after submitting a form, we also redirect to prevent resubmission of data when the page is refreshed in the browser. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Since a component is a reusable element and should not usually have a direct dependency on specific presenters, the `redirect()` and `link()` methods automatically interpret the parameter as a component signal: + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +If you need to redirect to a different presenter or action, you can do so through the presenter: + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Persistent Parameters ===================== @@ -347,7 +369,7 @@ services: Finally, we will use this factory in our presenter: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ Components in Depth Components in a Nette Application are the reusable parts of a web application that we embed in pages, which is the subject of this chapter. What exactly are the capabilities of such a component? 1) it is renderable in a template -2) it knows which part of itself to render during an [AJAX request |ajax#invalidation] (snippets) +2) it knows [which part of itself |ajax#snippets] to render during an AJAX request (snippets) 3) it has the ability to store its state in a URL (persistent parameters) 4) has the ability to respond to user actions (signals) 5) creates a hierarchical structure (where the root is the presenter) @@ -430,7 +452,7 @@ class PaginatingControl extends Control } ``` -The opposite process, that is, collecting values from persistent properites, is handled by the `saveState()` method. +The opposite process, that is, collecting values from persistent properties, is handled by the `saveState()` method. Signals in Depth @@ -444,7 +466,7 @@ Signal can be received by any component, presenter of object which implements in The main receivers of signals are `Presenters` and visual components extending `Control`. A signal is a sign for an object that it has to do something - poll counts in a vote from user, box with news has to unfold, form was sent and has to process data and so on. -The URL for the signal is created using the method [Component::link() |api:Nette\Application\UI\Component::link()]. As parameter `$destination` we pass string `{signal}!` and as `$args` an array of arguments which we want to pass to the signal handler. Signal parameters are attached to the URL of the current presenter/view. **The parameter `?do` in the URL determines the signal called.** +The URL for the signal is created using the [Component::link() |api:Nette\Application\UI\Component::link()] method. We pass the string `{signal}!` as the `$destination` parameter and the array of arguments we want to pass to the signal as `$args`. The signal is always called on the current presenter and action with the current parameters, the signal parameters are just added. In addition, the **parameter `?do`, which specifies the signal** is added right at the beginning. Its format is `{signal}` or `{signalReceiver}-{signal}`. `{signalReceiver}` is the name of the component in the presenter. This is why hyphen (inaccurately dash) can't be present in the name of components - it is used to divide the name of the component and signal, but it's possible to compose several components. diff --git a/application/en/configuration.texy b/application/en/configuration.texy index 208194e78f..78ff08d5aa 100644 --- a/application/en/configuration.texy +++ b/application/en/configuration.texy @@ -14,10 +14,14 @@ application: debugger: ... # (bool) defaults to true # will error-presenter be called on error? - catchExceptions: ... # (bool) defaults to true in production mode + # has effect only in developer mode + catchExceptions: ... # (bool) defaults to true # name of error-presenter - errorPresenter: Error # (string) defaults to 'Nette:Error' + errorPresenter: Error # (string|array) defaults to 'Nette:Error' + + # defines aliases for presenters and events + aliases: ... # defines the rules for resolving the presenter name to a class mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) defaults to false ``` -Because error-presenters are not called by default in development mode and the errors are displayed by Tracy, changing the value `catchExceptions` to `true` helps to verify that error-presenters works correct during development. +As of `nette/application` version 3.2 it is possible to define a pair of error-presenters: + +```neon +application: + errorPresenter: + 4xx: Error4xx # for Nette\Application\BadRequestException + 5xx: Error5xx # for other exceptions +``` Option `silentLinks` determines how Nette behaves in developer mode when link generation fails (for example, because there is no presenter, etc). The default value `false` means that Nette triggers `E_USER_WARNING`. Setting to `true` suppresses this error message. In a production environment, `E_USER_WARNING` is always invoked. We can also influence this behavior by setting the presenter variable [$invalidLinkMode |creating-links#Invalid Links]. -The [mapping defines the rules |modules#mapping] by which the class name is derived from the presenter name. +[Aliases simplify referencing |creating-links#aliases] frequently used presenters. + +The [mapping defines the rules |directory-structure#Presenter Mapping] by which the class name is derived from the presenter name. Automatic Registration of Presenters @@ -82,6 +95,9 @@ latte: # enables [checking generated code |latte:develop#Checking Generated Code] phpLinter: ... # (string) default is null + # sets the locale + locale: cs_CZ # (string) default is null + # class of $this->template templateClass: App\MyTemplateClass # defaults to Nette\Bridges\ApplicationLatte\DefaultTemplate ``` @@ -91,7 +107,7 @@ If you are using Latte version 3, you can add new [extension |latte:creating-ext ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` If you are using Latte version 2, you can register new tags either by entering the class name or by referring to the service. Method `install()` is called by default, but this can be changed by specifying the name of another method: diff --git a/application/en/creating-links.texy b/application/en/creating-links.texy index 0fb783c609..437ad53f5b 100644 --- a/application/en/creating-links.texy +++ b/application/en/creating-links.texy @@ -38,7 +38,7 @@ It is also possible to pass named parameters. The following link passes paramete detail ``` -If method `ProductPresenter::renderShow()` does not have `$lang` in its signature, it can read the value of the parameter using `$lang = $this->getParameter('lang')`. +If the method `ProductPresenter::renderShow()` does not have `$lang` in its signature, it can retrieve the value of the parameter using `$lang = $this->getParameter('lang')` or from the [property |presenters#Request Parameters]. If the parameters are stored in an array, they can be expanded with the `...` operator (or `(expand)` operator in Latte 2.x): @@ -103,7 +103,7 @@ If the action is `default`, we can omit it, but the colon must remain: home ``` -Links may also point to other [modules]. Here, the links are distinguished into relative to the submodules, or absolute. The principle is analogous to disk paths, only instead of slashes there are colons. Let's assume that the actual presenter is part of module `Front`, then we will write: +Links may also point to other [modules|directory-structure#Presenters and Templates]. Here, the links are distinguished into relative to the submodules, or absolute. The principle is analogous to disk paths, only instead of slashes there are colons. Let's assume that the actual presenter is part of module `Front`, then we will write: ```latte link to Front:Shop:Product:show @@ -140,7 +140,7 @@ The target `this` will create a link to the current page: refresh ``` -At the same time, all parameters specified in the signature of the `render()` or `action()` method are transferred. So if we are on the `Product:show` and `id:123` pages, the link to `this` will also pass this parameter. +At the same time, all parameters specified in the signature of the `action()` or `render()` method, if the `action()` is not defined, are transferred. So if we are on the `Product:show` and `id:123` pages, the link to `this` will also pass this parameter. Of course, it is possible to specify the parameters directly: @@ -213,7 +213,7 @@ Because [components] are separate reusable units that should have no relations t If we want to link to presenters in the component template, we use the tag `{plink}`: ```latte -home +home ``` or in the code @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Aliases .{data-version:v3.2.2} +============================== + +Sometimes it's useful to assign an easily memorable alias to a Presenter:action pair. For example, you could name the homepage `Front:Home:default` simply as `home` or `Admin:Dashboard:default` as `admin`. + +Aliases are defined in the [configuration|configuration] under the key `application › aliases`: + +```neon +application: + aliases: + home: Front:Home:default + admin: Admin:Dashboard:default + sign: Front:Sign:in +``` + +In links, they are written using the at symbol, for example: + +```latte +administration +``` + +They are supported in all methods that work with links, such as `redirect()` and similar. + + Invalid Links ============= @@ -257,6 +281,6 @@ How to create links with the method `link()` comfort, but without the presence o LinkGenerator is a service that you can have passed through the constructor and then create links using its method `link()`. -There is a difference compared to presenters. LinkGenerator creates all links as absolute URLs. Furthermore, there is no "current presenter", so it is not possible to specify only the name of the action `link('default')` or the relative paths to the [modules]. +Compared to presenters, there's a difference. LinkGenerator creates all links directly as absolute URLs. Also, there is no "actual presenter", so you can't just list the action name `link('default')` as the target or list relative paths to modules. Invalid links always throw `Nette\Application\UI\InvalidLinkException`. diff --git a/application/en/directory-structure.texy b/application/en/directory-structure.texy new file mode 100644 index 0000000000..553a288c8b --- /dev/null +++ b/application/en/directory-structure.texy @@ -0,0 +1,526 @@ +Directory Structure of the Application +************************************** + +
    + +How to design a clear and scalable directory structure for projects in Nette Framework? We'll show you proven practices that will help you organize your code. You'll learn: + +- how to **logically structure** the application into directories +- how to design the structure to **scale well** as the project grows +- what are the **possible alternatives** and their advantages or disadvantages + +
    + + +It's important to mention that Nette Framework itself doesn't insist on any specific structure. It's designed to be easily adaptable to any needs and preferences. + + +Basic Project Structure +======================= + +Although Nette Framework doesn't dictate any fixed directory structure, there is a proven default arrangement in the form of [Web Project|https://github.com/nette/web-project]: + +/--pre +web-project/ +├── app/ ← application directory +├── assets/ ← SCSS, JS files, images..., alternatively resources/ +├── bin/ ← command line scripts +├── config/ ← configuration +├── log/ ← logged errors +├── temp/ ← temporary files, cache +├── tests/ ← tests +├── vendor/ ← libraries installed by Composer +└── www/ ← public directory (document-root) +\-- + +You can freely modify this structure according to your needs - rename or move folders. Then you just need to adjust the relative paths to directories in `Bootstrap.php` and possibly `composer.json`. Nothing else is needed, no complex reconfiguration, no constant changes. Nette has smart autodetection and automatically recognizes the application location including its URL base. + + +Code Organization Principles +============================ + +When you first explore a new project, you should be able to quickly orient yourself. Imagine clicking on the `app/Model/` directory and seeing this structure: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +From this, you'll only learn that the project uses some services, repositories and entities. You won't learn anything about the actual purpose of the application. + +Let's look at a different approach - **organization by domains**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +This is different - at first glance it's clear that this is an e-commerce site. The directory names themselves reveal what the application can do - it works with payments, orders and products. + +The first approach (organization by class type) brings several problems in practice: code that is logically related is scattered across different folders and you have to jump between them. Therefore, we will organize by domains. + + +Namespaces +---------- + +It is conventional that the directory structure corresponds to namespaces in the application. This means that the physical location of files corresponds to their namespace. For example, a class located in `app/Model/Product/ProductRepository.php` should have namespace `App\Model\Product`. This principle helps in code orientation and simplifies autoloading. + + +Singular vs Plural in Names +--------------------------- + +Notice that we use singular for main application directories: `app`, `config`, `log`, `temp`, `www`. The same applies inside the application: `Model`, `Core`, `Presentation`. This is because each represents one unified concept. + +Similarly, `app/Model/Product` represents everything about products. We don't call it `Products` because it's not a folder full of products (that would contain files like `iphone.php`, `samsung.php`). It's a namespace containing classes for working with products - `ProductRepository.php`, `ProductService.php`. + +The folder `app/Tasks` is plural because it contains a set of standalone executable scripts - `CleanupTask.php`, `ImportTask.php`. Each of them is an independent unit. + +For consistency, we recommend using: +- Singular for namespaces representing a functional unit (even if working with multiple entities) +- Plural for collections of independent units +- In case of uncertainty or if you don't want to think about it, choose singular + + +Public Directory `www/` +======================= + +This directory is the only one accessible from the web (so-called document-root). You may often encounter the name `public/` instead of `www/` - it's just a matter of convention and doesn't affect the functionality. The directory contains: +- Application [entry point |bootstrap#index.php] `index.php` +- `.htaccess` file with mod_rewrite rules (for Apache) +- Static files (CSS, JavaScript, images) +- Uploaded files + +For proper application security, it's crucial to have correctly [configured document-root |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url]. + +.[note] +Never place the `node_modules/` folder in this directory - it contains thousands of files that may be executable and shouldn't be publicly accessible. + + +Application Directory `app/` +============================ + +This is the main directory with application code. Basic structure: + +/--pre +app/ +├── Core/ ← infrastructure matters +├── Model/ ← business logic +├── Presentation/ ← presenters and templates +├── Tasks/ ← command scripts +└── Bootstrap.php ← application bootstrap class +\-- + +`Bootstrap.php` is the [application startup class|bootstrap] that initializes the environment, loads configuration and creates the DI container. + +Let's now look at individual subdirectories in detail. + + +Presenters and Templates +======================== + +We have the presentation part of the application in the `app/Presentation` directory. An alternative is the short `app/UI`. This is the place for all presenters, their templates and any helper classes. + +We organize this layer by domains. In a complex project that combines e-commerce, blog and API, the structure would look like this: + +/--pre +app/Presentation/ +├── Shop/ ← e-commerce frontend +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← blog +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administration +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← API endpoints + └── V1/ +\-- + +Conversely, for a simple blog we would use this structure: + +/--pre +app/Presentation/ +├── Front/ ← website frontend +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administration +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, sitemaps etc. +\-- + +Folders like `Home/` or `Dashboard/` contain presenters and templates. Folders like `Front/`, `Admin/` or `Api/` are called **modules**. Technically, these are regular directories that serve for logical organization of the application. + +Each folder with a presenter contains a similarly named presenter and its templates. For example, the `Dashboard/` folder contains: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← presenter +└── default.latte ← template +\-- + +This directory structure is reflected in class namespaces. For example, `DashboardPresenter` is in the namespace `App\Presentation\Admin\Dashboard` (see [#presenter mapping]): + +```php +namespace App\Presentation\Admin\Dashboard; + +class DashboardPresenter extends Nette\Application\UI\Presenter +{ + // ... +} +``` + +We refer to the `Dashboard` presenter inside the `Admin` module in the application using colon notation as `Admin:Dashboard`. To its `default` action then as `Admin:Dashboard:default`. For nested modules, we use more colons, for example `Shop:Order:Detail:default`. + + +Flexible Structure Development +------------------------------ + +One of the great advantages of this structure is how elegantly it adapts to growing project needs. As an example, let's take the part generating XML feeds. Initially, we have a simple form: + +/--pre +Export/ +├── ExportPresenter.php ← one presenter for all exports +├── sitemap.latte ← template for sitemap +└── feed.latte ← template for RSS feed +\-- + +Over time, more feed types are added and we need more logic for them... No problem! The `Export/` folder simply becomes a module: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← feed for Amazon + └── ebay.latte ← feed for eBay +\-- + +This transformation is completely smooth - just create new subfolders, divide the code into them and update links (e.g. from `Export:feed` to `Export:Feed:amazon`). Thanks to this, we can gradually expand the structure as needed, the nesting level is not limited in any way. + +For example, if in the administration you have many presenters related to order management, such as `OrderDetail`, `OrderEdit`, `OrderDispatch` etc., you can create a module (folder) `Order` for better organization, which will contain (folders for) presenters `Detail`, `Edit`, `Dispatch` and others. + + +Template Location +----------------- + +In previous examples, we saw that templates are located directly in the folder with the presenter: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← presenter +├── DashboardTemplate.php ← optional template class +└── default.latte ← template +\-- + +This location proves to be the most convenient in practice - you have all related files right at hand. + +Alternatively, you can place templates in a `templates/` subfolder. Nette supports both variants. You can even place templates completely outside the `Presentation/` folder. Everything about template location options can be found in the [Template Lookup|templates#Template Lookup] chapter. + + +Helper Classes and Components +----------------------------- + +Presenters and templates often come with other helper files. We place them logically according to their scope: + +1. **Directly with the presenter** in case of specific components for the given presenter: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← component for product listing +└── FilterForm.php ← form for filtering +\-- + +2. **For module** - we recommend using the `Accessory` folder, which is placed neatly at the beginning of the alphabet: + +/--pre +Front/ +├── Accessory/ +│ ├── NavbarControl.php ← components for frontend +│ └── TemplateFilters.php +├── Product/ +└── Cart/ +\-- + +3. **For entire application** - in `Presentation/Accessory/`: +/--pre +app/Presentation/ +├── Accessory/ +│ ├── LatteExtension.php +│ └── TemplateFilters.php +├── Front/ +└── Admin/ +\-- + +Or you can place helper classes like `LatteExtension.php` or `TemplateFilters.php` in the infrastructure folder `app/Core/Latte/`. And components in `app/Components`. The choice depends on team conventions. + + +Model - Heart of the Application +================================ + +The model contains all business logic of the application. For its organization, the same rule applies - we structure by domains: + +/--pre +app/Model/ +├── Payment/ ← everything about payments +│ ├── PaymentFacade.php ← main entry point +│ ├── PaymentRepository.php +│ ├── Payment.php ← entity +├── Order/ ← everything about orders +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← everything about shipping +\-- + +In the model, you typically encounter these types of classes: + +**Facades**: represent the main entry point into a specific domain in the application. They act as an orchestrator that coordinates cooperation between different services to implement complete use-cases (like "create order" or "process payment"). Under their orchestration layer, the facade hides implementation details from the rest of the application, thus providing a clean interface for working with the given domain. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // validation + // order creation + // email sending + // writing to statistics + } +} +``` + +**Services**: focus on specific business operations within a domain. Unlike facades that orchestrate entire use-cases, a service implements specific business logic (like price calculations or payment processing). Services are typically stateless and can be used either by facades as building blocks for more complex operations, or directly by other parts of the application for simpler tasks. + +```php +class PricingService +{ + public function calculateTotal(Order $order): Money + { + // price calculation + } +} +``` + +**Repositories**: handle all communication with the data storage, typically a database. Their task is to load and save entities and implement methods for searching them. A repository shields the rest of the application from database implementation details and provides an object-oriented interface for working with data. + +```php +class OrderRepository +{ + public function find(int $id): ?Order + { + } + + public function findByCustomer(int $customerId): array + { + } +} +``` + +**Entities**: objects representing main business concepts in the application that have their identity and change over time. Typically these are classes mapped to database tables using ORM (like Nette Database Explorer or Doctrine). Entities can contain business rules concerning their data and validation logic. + +```php +// Entity mapped to database table 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**: immutable objects representing values without their own identity - for example, a money amount or email address. Two instances of a value object with the same values are considered identical. + + +Infrastructure Code +=================== + +The `Core/` folder (or also `Infrastructure/`) is home to the technical foundation of the application. Infrastructure code typically includes: + +/--pre +app/Core/ +├── Router/ ← routing and URL management +│ └── RouterFactory.php +├── Security/ ← authentication and authorization +│ ├── Authenticator.php +│ └── Authorizator.php +├── Logging/ ← logging and monitoring +│ ├── SentryLogger.php +│ └── FileLogger.php +├── Cache/ ← caching layer +│ └── FullPageCache.php +└── Integration/ ← integration with ext. services + ├── Slack/ + └── Stripe/ +\-- + +For smaller projects, a flat structure is naturally sufficient: + +/--pre +Core/ +├── RouterFactory.php +├── Authenticator.php +└── QueueMailer.php +\-- + +This is code that: + +- Handles technical infrastructure (routing, logging, caching) +- Integrates external services (Sentry, Elasticsearch, Redis) +- Provides basic services for the entire application (mail, database) +- Is mostly independent of specific domain - cache or logger works the same for e-commerce or blog. + +Wondering if a certain class belongs here or in the model? The key difference is that code in `Core/`: + +- Knows nothing about the domain (products, orders, articles) +- Can usually be transferred to another project +- Solves "how it works" (how to send mail), not "what it does" (what mail to send) + +Example for better understanding: + +- `App\Core\MailerFactory` - creates instances of email sending class, handles SMTP settings +- `App\Model\OrderMailer` - uses `MailerFactory` to send emails about orders, knows their templates and when they should be sent + + +Command Scripts +=============== + +Applications often need to perform tasks outside of regular HTTP requests - whether it's background data processing, maintenance, or periodic tasks. Simple scripts in the `bin/` directory are used for execution, while the actual implementation logic is placed in `app/Tasks/` (or `app/Commands/`). + +Example: + +/--pre +app/Tasks/ +├── Maintenance/ ← maintenance scripts +│ ├── CleanupCommand.php ← deleting old data +│ └── DbOptimizeCommand.php ← database optimization +├── Integration/ ← integration with external systems +│ ├── ImportProducts.php ← import from supplier system +│ └── SyncOrders.php ← order synchronization +└── Scheduled/ ← regular tasks + ├── NewsletterCommand.php ← sending newsletters + └── ReminderCommand.php ← customer notifications +\-- + +What belongs in the model and what in command scripts? For example, logic for sending one email is part of the model, bulk sending of thousands of emails belongs in `Tasks/`. + +Tasks are usually [run from command line |https://blog.nette.org/en/cli-scripts-in-nette-application] or via cron. They can also be run via HTTP request, but security must be considered. The presenter that runs the task needs to be secured, for example only for logged-in users or with a strong token and access from allowed IP addresses. For long tasks, it's necessary to increase the script time limit and use `session_write_close()` to avoid locking the session. + + +Other Possible Directories +========================== + +In addition to the mentioned basic directories, you can add other specialized folders according to project needs. Let's look at the most common ones and their use: + +/--pre +app/ +├── Api/ ← API logic independent of presentation layer +├── Database/ ← migration scripts and seeders for test data +├── Components/ ← shared visual components across the application +├── Event/ ← useful if using event-driven architecture +├── Mail/ ← email templates and related logic +└── Utils/ ← helper classes +\-- + +For shared visual components used in presenters across the application, you can use the `app/Components` or `app/Controls` folder: + +/--pre +app/Components/ +├── Form/ ← shared form components +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← components for data listings +│ └── DataGrid.php +└── Navigation/ ← navigation elements + ├── Breadcrumbs.php + └── Menu.php +\-- + +This is where components with more complex logic belong. If you want to share components between multiple projects, it's good to separate them into a standalone composer package. + +In the `app/Mail` directory you can place email communication management: + +/--pre +app/Mail/ +├── templates/ ← email templates +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Presenter Mapping +================= + +Mapping defines rules for deriving class names from presenter names. We specify them in the [configuration|configuration] under the key `application › mapping`. + +On this page, we've shown that we place presenters in the `app/Presentation` folder (or `app/UI`). We need to tell Nette about this convention in the configuration file. One line is enough: + +```neon +application: + mapping: App\Presentation\*\**Presenter +``` + +How does mapping work? To better understand, let's first imagine an application without modules. We want presenter classes to fall under the `App\Presentation` namespace, so that the `Home` presenter maps to the `App\Presentation\HomePresenter` class. This is achieved with this configuration: + +```neon +application: + mapping: App\Presentation\*Presenter +``` + +Mapping works by replacing the asterisk in the mask `App\Presentation\*Presenter` with the presenter name `Home`, resulting in the final class name `App\Presentation\HomePresenter`. Simple! + +However, as you see in examples in this and other chapters, we place presenter classes in eponymous subdirectories, for example the `Home` presenter maps to class `App\Presentation\Home\HomePresenter`. We achieve this by doubling the colon (requires Nette Application 3.2): + +```neon +application: + mapping: App\Presentation\**Presenter +``` + +Now we'll move on to mapping presenters into modules. We can define specific mapping for each module: + +```neon +application: + mapping: + Front: App\Presentation\Front\**Presenter + Admin: App\Presentation\Admin\**Presenter + Api: App\Api\*Presenter +``` + +According to this configuration, the presenter `Front:Home` maps to class `App\Presentation\Front\Home\HomePresenter`, while presenter `Api:OAuth` maps to class `App\Api\OAuthPresenter`. + +Because modules `Front` and `Admin` have a similar mapping method and there will probably be more such modules, it's possible to create a general rule that will replace them. A new asterisk for the module will be added to the class mask: + +```neon +application: + mapping: + *: App\Presentation\*\**Presenter + Api: App\Api\*Presenter +``` + +It also works for deeper nested directory structures, such as presenter `Admin:User:Edit`, where the segment with asterisk repeats for each level and results in class `App\Presentation\Admin\User\Edit\EditPresenter`. + +An alternative notation is to use an array consisting of three segments instead of a string. This notation is equivalent to the previous one: + +```neon +application: + mapping: + *: [App\Presentation, *, **Presenter] + Api: [App\Api, '', *Presenter] +``` diff --git a/application/en/how-it-works.texy b/application/en/how-it-works.texy index 08d7c3eb8b..57be02d32f 100644 --- a/application/en/how-it-works.texy +++ b/application/en/how-it-works.texy @@ -22,18 +22,18 @@ The directory structure looks something like this: /--pre web-project/ ├── app/ ← directory with application -│ ├── Presenters/ ← presenter classes -│ │ ├── HomePresenter.php ← Home presenter class -│ │ └── templates/ ← templates directory -│ │ ├── @layout.latte ← template of shared layout -│ │ └── Home/ ← templates for Home presenter -│ │ └── default.latte ← template for action `default` -│ ├── Router/ ← configuration of URL addresses +│ ├── Core/ ← basic necessary classes +│ │ └── RouterFactory.php ← configuration of URL addresses +│ ├── Presentation/ ← presenters, templates & co. +│ │ ├── @layout.latte ← template of shared layout +│ │ └── Home/ ← Home presenter directory +│ │ ├── HomePresenter.php ← Home presenter class +│ │ └── default.latte ← template for action default │ └── Bootstrap.php ← booting class Bootstrap ├── bin/ ← scripts for the command line ├── config/ ← configuration files │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← error logs ├── temp/ ← temporary files, cache, … ├── vendor/ ← libraries installed by Composer @@ -45,9 +45,9 @@ The directory structure looks something like this: └── .htaccess ← prohibits access to all directories except www \-- -You can change the directory structure in any way, rename or move folders, and then just edit the paths to `log/` and `temp/` in the `Bootstrap.php` file and the path to this file in `composer.json` in the `autoload` section. Nothing more, no complicated reconfiguration, no constant changes. Nette has a [smart autodetection|bootstrap#development-vs-production-mode]. +You can modify the directory structure however you like, rename or move folders - it's completely flexible. Nette also features smart autodetection and automatically recognizes the application location including its URL base. -For slightly larger applications, we can divide folders with presenters and templates into subdirectories (on disk) and into namespaces (in code), which we call [modules]. +For slightly larger applications, we can organize presenter and template folders into [subdirectories |directory-structure#Presenters and templates] and group classes into namespaces, which we call modules. The `www/` directory is the public directory or document-root of the project. You can rename it without having to set anything else on the application side. You just need to [configure the hosting |nette:troubleshooting#How to change or remove www directory from URL] so that the document-root goes to this directory. @@ -75,7 +75,7 @@ Its task is: What kind of factory? We do not produce tractors, but websites! Hold on, it'll be explained right away. -By "initialize the environment" we mean, for example, that [Tracy |tracy:] is activated, which is an amazing tool for logging or visualizing errors. It logs errors on the production server and displays them directly on the development server. Therefore, initialization also needs to decide whether the site is running in production or developer mode. To do this, Nette uses autodetection: if you run the site on localhost, it runs in developer mode. You don't have to configure anything and the application is ready for both development and production deployment. These steps are performed and described in detail in the chapter about [Bootstrap class |bootstrap]. +By "environment initialization" we mean, for example, activating [Tracy|tracy:], which is a fantastic tool for logging and error visualization. On production servers it logs errors, while on development it displays them directly. Therefore, initialization includes determining whether the website runs in production or development mode. For this, Nette uses [smart autodetection|bootstrap#development-vs-production-mode]: if you run the site on localhost, it operates in development mode. No configuration is needed and the application is ready for both development and production deployment. These steps are performed and detailed in the [Bootstrap class|bootstrap] chapter. The third point (yes, we skipped the second, but we will return to it) is to start the application. The handling of HTTP requests in Nette is done by the class `Nette\Application\Application` (hereinafter referred to as the `Application`), so when we say "run an application", we mean to call a method with the name `run()` on an object of this class. @@ -91,7 +91,7 @@ Applications written in Nette are divided into many so-called presenters (in oth The application starts by asking the so-called router to decide which of the presenters to pass the current request for processing. The router decides whose responsibility it is. It looks at the input URL `https://example.com/product/123` and, based on how it is set up, decides that this is a job, for example, for **presenter** `Product`, who wants to `show` a product with `id: 123` as an action. It is a good habit to write a pairs of presenter + action separated by a colon as `Product:show`. -So the router transformed the URL into a pair `Presenter:action` + parameters, in our case `Product:show` + `id: 123`. You can see how a router looks like in file `app/Router/RouterFactory.php` and we will describe it in detail in chapter [Routing]. +So the router transformed the URL into a pair `Presenter:action` + parameters, in our case `Product:show` + `id: 123`. You can see how a router looks like in file `app/Core/RouterFactory.php` and we will describe it in detail in chapter [Routing]. Let's move on. The application already knows the name of the presenter and can continue. By creating an object `ProductPresenter`, which is the code of presenter `Product`. More precisely, it asks the DI container for creating the presenter, because producting objects is its job. @@ -121,12 +121,9 @@ So, the method `renderShow(123)` was called, whose code is fictional example, bu Subsequently, the presenter returns the answer. This can be an HTML page, an image, an XML document, sending a file from disk, JSON or redirecting to another page. Importantly, if we do not explicitly say how to respond (which is the case of `ProductPresenter`), the answer will be to render the template with an HTML page. Why? Well, because in 99% of cases we want to draw a template, so the presenter takes this behavior as the default and wants to make our work easier. That's Nette's point. -We don't even have to state which template to draw, he derives the path to it according to simple logic. In the case of presenter `Product` and action `show`, it tries to see if one of these template files exists relative to the directory where class `ProductPresenter` is located: +We don't even need to specify which template to render; the framework will deduce the path itself. In the case of the `show` action, it simply tries to load the `show.latte` template in the directory with the `ProductPresenter` class. It also attempts to find the layout in the `@layout.latte` file (more about [template searching |templates#Template Lookup]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -It will also try to find the layout in file `@layout.latte` and then it renders the template. Now the task of the presenter and the entire application is completed. If the template does not exist, a page with error 404 will be returned. You can read more about presenters on the [Presenters] page. +Subsequently, the templates are rendered. This completes the task of the presenter and the entire application, and the work is done. If the template did not exist, a 404 error page would be returned. You can read more about presenters on the page [Presenters|presenters]. [* request-flow.svg *] @@ -137,7 +134,7 @@ Just to be sure, let's try to recap the whole process with a slightly different 3) the router decodes the URL as a pair `Home:default` 4) an `HomePresenter` object is created 5) method `renderDefault()` is called (if exists) -6) a template `templates/Home/default.latte` with a layout `templates/@layout.latte` is rendered +6) a template `default.latte` with a layout `@layout.latte` is rendered You may have come across a lot of new concepts now, but we believe they make sense. Creating applications in Nette is a breeze. diff --git a/application/en/modules.texy b/application/en/modules.texy deleted file mode 100644 index 6312143fdd..0000000000 --- a/application/en/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Modules -******* - -.[perex] -In Nette, modules represent the logical units that make up an application. They include presenters, templates, possibly also components and model classes. - -One directory for presenters and one for templates would not be enough for real projects. Having dozens of files in one folder is at least unorganized. How to get out of it? We simply split them into subdirectories on disk and into namespaces in the code. And that's exactly what the Nette modules do. - -So let's forget about a single folder for presenters and templates and instead create modules, for example `Admin` and `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← directory with modules -│ ├── Admin/ ← module Admin -│ │ ├── Presenters/ ← its presenters -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← module Front -│ └── Presenters/ ← its presenters -│ └── ... -\-- - -This directory structure will be reflected by the class namespaces, so for example `DashboardPresenter` will be in the `App\Modules\Admin\Presenters` namespace: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -The `Dashboard` presenter inside the `Admin` module is referenced within the application using the colon notation as `Admin:Dashboard`, and its `default` action as `Admin:Dashboard:default`. -And how does Nette proper know that `Admin:Dashboard` represents the `App\Modules\Admin\Presenters\DashboardPresenter` class? This is determined by [#mapping] in the configuration. -Thus, the given structure is not hard set and you can modify it according to your needs. - -Modules can of course contain all other items besides presenters and templates, such as components, model classes, etc. - - -Nested Modules --------------- - -Modules don't have to form only a flat structure, you can also create submodules, for example: - -/--pre -app/ -├── Modules/ ← directory with modules -│ ├── Blog/ ← module Blog -│ │ ├── Admin/ ← submodule Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← submodule Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← module Forum -│ │ └── ... -\-- - -Thus, the `Blog` module is divided into `Admin` and `Front` submodules. Again, this will be reflected in the namespaces, which will be `App\Modules\Blog\Admin\Presenters` etc. The presenter `Dashboard` inside the submodule is referred to as `Blog:Admin:Dashboard`. - -The nesting can go as deep as you like, so sub-submodules can be created. - - -Creating Links --------------- - -Links in presenter templates are relative to the current module. Thus, the link `Foo:default` leads to the presenter `Foo` in the same module as the current presenter. If the current module is `Front`, for example, then the link goes like this: - -```latte -link to Front:Product:show -``` - -A link is relative even if it includes the name of a module, which is then considered a submodule: - -```latte -link to Front:Shop:Product:show -``` - -Absolute links are written analogously to absolute paths on disk, but with colons instead of slashes. Thus, an absolute link starts with a colon: - -```latte -link to Admin:Product:show -``` - -To find out if we are in a certain module or its submodule we can use `isModuleCurrent(moduleName)` function. - -```latte -
  • - ... -
  • -``` - - -Routing -------- - -See [chapter on routing |routing#Modules]. - - -Mapping -------- - -Defines the rules by which the class name is derived from the presenter name. We write them in [configuration] under the `application › mapping` key. - -Let's start with a sample that doesn't use modules. We'll just want the presenter classes to have the `App\Presenters` namespace. That means that a presenter such as `Home` should map to the `App\Presenters\HomePresenter` class. This can be achieved by the following configuration: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -The presenter name is replaced with the asterisk in the class mask and the result is the class name. Easy! - -If we divide presenters into modules, we can have our own mapping for each module: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Now presenter `Front:Home` maps to class `App\Modules\Front\Presenters\HomePresenter` and presenter `Admin:Dashboard` to class `App\Modules\Admin\Presenters\DashboardPresenter`. - -It is more practical to create a general (star) rule to replace the first two. The extra asterisk will be added to the class mask just for the module: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -But what if we use nested modules and have a presenter `Admin:User:Edit`? In this case, the segment with an asterisk representing the module for each level is simply repeated and the result is class `App\Modules\Admin\User\Presenters\EditPresenter`. - -An alternative notation is to use an array consisting of three segments instead of a string. This notation is equivalent to the previous one: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -The default value is `*: *Module\*Presenter`. diff --git a/application/en/presenters.texy b/application/en/presenters.texy index e653f0945c..2773f8486b 100644 --- a/application/en/presenters.texy +++ b/application/en/presenters.texy @@ -60,7 +60,7 @@ Similar to the method `render()`. While `render()` is intended to pr It is important that `action()` is called before `render()`, so inside it we can possibly change the next course of life cycle, i.e. change the template that will be rendered and also the method `render()` that will be called, using `setView('otherView')`. -The parameters from the request are passed to the method. It is possible and recommended to specify types for the parameters, e.g. `actionShow(int $id, string $slug = null)` - if parameter `id` is missing or if it is not an integer, the presenter returns [error 404|#Error 404 etc.] and terminates the operation. +The parameters from the request are passed to the method. It is possible and recommended to specify types for the parameters, e.g. `actionShow(int $id, ?string $slug = null)` - if parameter `id` is missing or if it is not an integer, the presenter returns [error 404|#Error 404 etc.] and terminates the operation. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ In the template, these messages are available in the variable `$flashes` as obje Error 404 etc. ============== -When we can't fulfill the request because for example the article we want to display does not exist in the database, we will throw out the 404 error using method `error(string $message = null, int $httpCode = 404)`, which represents HTTP error 404: +When we can't fulfill the request because for example the article we want to display does not exist in the database, we will throw out the 404 error using method `error(?string $message = null, int $httpCode = 404)`, which represents HTTP error 404: ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Request Parameters .{data-version:3.1.14} +========================================= + +The presenter, as well as every component, obtains its parameters from the HTTP request. Their values can be retrieved using the `getParameter($name)` method or `getParameters()`. The values are strings or arrays of strings, essentially raw data obtained directly from the URL. + +For added convenience, we recommend making parameters accessible through properties. Simply annotate them with the `#[Parameter]` attribute: + +```php +use Nette\Application\Attributes\Parameter; // this line is important + +class HomePresenter extends Nette\Application\UI\Presenter +{ + #[Parameter] + public string $theme; // must be public +} +``` + +For properties, we suggest specifying the data type (e.g., `string`). Nette will then automatically cast the value based on it. Parameter values can be also [validated |#Validation of Parameters]. + +When creating a link, you can directly set the value for the parameters: + +```latte +click +``` + + Persistent Parameters ===================== @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter If `$this->lang` has a value such as `'en'`, then links created using `link()` or `n:href` will also contain the `lang=en` parameter. And when the link is clicked, it will again be `$this->lang = 'en'`. -For properties, we recommend that you include the data type (e.g. `string`) and you can also include a default value. Parameter values can be [validated |#Validation of Persistent Parameters]. +For properties, we recommend that you include the data type (e.g. `string`) and you can also include a default value. Parameter values can be [validated |#Validation of Parameters]. Persistent parameters are passed between all actions of a given presenter by default. To pass them between multiple presenters, you need to define them either: @@ -307,18 +333,12 @@ Going Deeper What we have shown so far in this chapter will probably suffice. The following lines are intended for those who are interested in presenters in depth and want to know everything. -Requirement and Parameters --------------------------- +Validation of Parameters +------------------------ -The request handled by the presenter is the [api:Nette\Application\Request] object and is returned by the presenter's method `getRequest()`. It includes an array of parameters and each of them belongs either to some of the components or directly to the presenter (which is actually also a component, albeit a special one). So Nette redistributes the parameters and passes between the individual components (and the presenter) by calling the method `loadState(array $params)`. The parameters can be obtained by the method `getParameters(): array`, individually using `getParameter($name)`. Parameter values ​​are strings or arrays of strings, they are basically raw data obtained directly from a URL. +The values of [#request parameters] and [#persistent parameters] received from URLs are written to properties by the `loadState()` method. It also checks if the data type specified in the property matches, otherwise it will respond with a 404 error and the page will not be displayed. - -Validation of Persistent Parameters ------------------------------------ - -The values of [#persistent parameters] received from URLs are written to properties by the `loadState()` method. It also checks if the data type specified in the property matches, otherwise it will respond with a 404 error and the page will not be displayed. - -Never blindly trust persistent parameters, as they can easily be overwritten by the user in the URL. For example, this is how we check if `$this->lang` is among the supported languages. A good way to do this is to override the `loadState()` method mentioned above: +Never blindly trust parameters, as they can easily be overwritten by the user in the URL. For example, this is how we check if `$this->lang` is among the supported languages. A good way to do this is to override the `loadState()` method mentioned above: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,6 +361,8 @@ class ProductPresenter extends Nette\Application\UI\Presenter Save and Restore the Request ---------------------------- +The request that the presenter handles is an object [api:Nette\Application\Request] and is returned by the presenter's method `getRequest()`. + You can save the current request to a session or restore it from the session and let the presenter execute it again. This is useful, for example, when a user fills out a form and its login expires. In order not to lose data, before redirecting to the sign-in page, we save the current request to the session using `$reqId = $this->storeRequest()`, which returns an identifier in the form of a short string and passes it as a parameter to the sign-in presenter. After sign in, we call the method `$this->restoreRequest($reqId)`, which picks up the request from the session and forwards it to it. The method verifies that the request was created by the same user as now logged in is. If another user logs in or the key is invalid, it does nothing and the program continues. @@ -362,7 +384,7 @@ Redirection does not occur with an AJAX or POST request because it would result You can also invoke canonization manually using method `canonicalize()`, which, like method `link()`, receives the presenter, actions, and parameters as arguments. It creates a link and compares it to the current URL. If it is different, it redirects to the generated link. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // redirects if $slug is different from $realSlug @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Access Restriction Using `#[Requires]` .{data-version:3.2.2} +------------------------------------------------------------ + +The `#[Requires]` attribute provides advanced options for restricting access to presenters and their methods. It can be used to specify HTTP methods, require AJAX requests, limit access to the same origin, and restrict access to forwarding only. The attribute can be applied to presenter classes as well as individual methods such as `action()`, `render()`, `handle()`, and `createComponent()`. + +You can specify these restrictions: +- on HTTP methods: `#[Requires(methods: ['GET', 'POST'])]` +- requiring an AJAX request: `#[Requires(ajax: true)]` +- access only from the same origin: `#[Requires(sameOrigin: true)]` +- access only via forwarding: `#[Requires(forward: true)]` +- restrictions on specific actions: `#[Requires(actions: 'default')]` + +For details, see [How to use the Requires attribute |best-practices:attribute-requires]. + + +HTTP Method Check +----------------- + +In Nette, presenters automatically verify the HTTP method of each incoming request primarily for security reasons. By default, the methods `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` are allowed. + +If you want to enable additional methods such as `OPTIONS`, you can use the `#[Requires]` attribute (from Nette Application v3.2): + +```php +#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] +class MyPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +In version 3.1, the verification is performed in `checkHttpMethod()`, which checks if the method specified in the request is included in the array `$presenter->allowedMethods`. Add a method like this: + +```php +class MyPresenter extends Nette\Application\UI\Presenter +{ + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } +} +``` + +It's crucial to emphasize that if you enable the `OPTIONS` method, you must also properly handle it within your presenter. This method is often used as a so-called preflight request, which browsers automatically send before the actual request when it's necessary to determine if the request is allowed from the standpoint of CORS (Cross-Origin Resource Sharing) policy. If you allow this method but do not implement an appropriate response, it can lead to inconsistencies and potential security issues. + + Further Reading =============== diff --git a/application/en/routing.texy b/application/en/routing.texy index c35c692adb..6c50739932 100644 --- a/application/en/routing.texy +++ b/application/en/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Or we can use this form, notice the rewriting of the validation regular expression: +For a more detailed specification, an even more extended form can be used, where in addition to default values, other parameter properties can be set, such as a validation regular expression (see the `id` parameter): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -These more talkative formats are useful for adding other metadata. +It is important to note that if the parameters defined in the array are not included in the path mask, their values cannot be changed, not even using query parameters specified after a question mark in the URL. Filters and Translations @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Modules ------- -If we have more routes that belong to one [module |modules], we can use `withModule()` to group them: +If we have more routes that belong to one [module |directory-structure#Presenters and Templates], we can use `withModule()` to group them: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Integration =========== -In order to connect the our router into the application, we must tell the DI container about it. The easiest way is to prepare the factory that will build the router object and tell the container configuration to use it. So let's say we write a method for this purpose `App\Router\RouterFactory::createRouter()`: +In order to connect the our router into the application, we must tell the DI container about it. The easiest way is to prepare the factory that will build the router object and tell the container configuration to use it. So let's say we write a method for this purpose `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ Then we write in [configuration |dependency-injection:services]: ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Any dependencies, such as a database connection etc., are passed to the factory method as its parameters using [autowiring |dependency-injection:autowiring]: @@ -663,7 +663,7 @@ By separated usage, we mean the use of the router's capabilities in an applicati So again we will create a method that will build a router, for example: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Or we will create objects directly: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/en/templates.texy b/application/en/templates.texy index 5107b4493a..40a4163344 100644 --- a/application/en/templates.texy +++ b/application/en/templates.texy @@ -34,35 +34,81 @@ And this might be the action template: It defines block `content`, which is inserted in place of `{include content}` in the layout, and also re-defines block `title`, which overwrites `{block title}` in the layout. Try to imagine the result. -Search for Templates --------------------- +Template Lookup +--------------- -The path to the templates is deduced according to simple logic. It tries to see if one of these template files exists relative to the directory where presenter class is located, where `` is the name of the current presenter and `` is the name of the current action: +In presenters, you don't need to specify which template should be rendered; the framework will automatically determine the path, making coding easier for you. -- `templates//.latte` -- `templates/..latte` +If you use a directory structure where each presenter has its own directory, simply place the template in this directory under the name of the action (i.e. view). For example, for the `default` action, use the `default.latte` template: -If the template is not found, it will try to search in the `templates` directory one level up, i.e., at the same level as the directory with the presenter class. +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -If the template is not found there either, the response is a [404 error|presenters#Error 404 etc.]. +If you use a structure where presenters are together in one directory and templates in a `templates` folder, save it either in a file `..latte` or `/.latte`: -You can also change the view using `$this->setView('otherView')`. Or, instead of searching, directly specify the name of the template file using `$this->template->setFile('/path/to/template.latte')`. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- + +The `templates` directory can also be placed one level higher, at the same level as the directory with presenter classes. + +If the template is not found, the presenter responds with [404 - page not found error|presenters#Error 404 etc]. + +You can change the view using `$this->setView('anotherView')`. It is also possible to directly specify the template file with `$this->template->setFile('/path/to/template.latte')`. .[note] -You can change the paths where templates are searched by overriding the [formatTemplateFiles |api:Nette\Application\UI\Presenter::formatTemplateFiles()] method, which returns an array of possible file paths. +Files where templates are searched can be changed by overriding the method [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], which returns an array of possible file names. + + +Layout Template Lookup +---------------------- + +Nette also automatically searches for the layout file. + +If you use a directory structure where each presenter has its own directory, place the layout either in the folder with the presenter, if it is specific only to them, or a level higher if it is common to multiple presenters: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +If you use a structure where presenters are grouped together in one directory and templates are in a `templates` folder, the layout will be expected in the following places: -The layout is expected in the following files: +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` layout common to multiple presenters +If the presenter is in a module, it will also search further up the directory tree according to the module's nesting. -`` is the name of the current presenter and `` is the name of the layout, which is by default `'layout'`. The name can be changed with `$this->setLayout('otherLayout')`, so that `@otherLayout.latte` files will be tried. +The name of the layout can be changed using `$this->setLayout('layoutAdmin')` and then it will be expected in the file `@layoutAdmin.latte`. You can also directly specify the layout template file using `$this->setLayout('/path/to/template.latte')`. -You can also directly specify the file name of the layout template using `$this->setLayout('/path/to/template.latte')`. Using `$this->setLayout(false)` will disable the layout searching. +Using `$this->setLayout(false)` or the `{layout none}` tag inside the template disables layout search. .[note] -You can change the paths where templates are searched by overriding the [formatLayoutTemplateFiles |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] method, which returns an array of possible file paths. +Files where layout templates are searched can be changed by overriding the method [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], which returns an array of possible file names. Variables in the Template @@ -104,7 +150,7 @@ The `@property-read` annotation is for IDE and static analysis, it will make aut You can indulge in the luxury of whispering in templates too, just install the Latte plugin in PhpStorm and specify the class name at the beginning of the template, see the article "Latte: how to type system":https://blog.nette.org/en/latte-how-to-use-type-system: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void Latte version 3 offers a more advanced way by creating an [extension |latte:creating-extension] for each web project. Here is a rough example of such a class: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ We register it using [configuration#Latte]: ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ Alternatively, the translator can be set using the [configuration |configuration ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` The translator can then be used, for example, as a filter `|translate`, with additional parameters passed to the `translate()` method (see `foo, bar`): diff --git a/application/es/@home.texy b/application/es/@home.texy index b2966f5062..708c39c397 100644 --- a/application/es/@home.texy +++ b/application/es/@home.texy @@ -2,35 +2,84 @@ Aplicación Nette **************** .[perex] -El paquete `nette/application` es la base para crear aplicaciones web interactivas. +Nette Application es el núcleo del framework Nette que aporta potentes herramientas para crear aplicaciones web modernas. Ofrece numerosas características excepcionales que simplifican significativamente el desarrollo y mejoran la seguridad y la mantenibilidad del código. -- [¿Cómo funcionan las aplicaciones? |how-it-works] -- [Bootstrap] -- [Presentadores |Presenters] -- [Plantillas |Templates] -- [Módulos |Modules] -- [Enrutamiento |Routing] -- [Creación de enlaces URL |creating-links] -- [Componentes interactivos |components] -- [AJAX y fragmentos |ajax] -- [Multiplicador |multiplier] -- [Configuración |Configuration] +Instalación .[#toc-installation] +-------------------------------- -Instalación ------------ - -Descargue e instale el paquete utilizando [Composer |best-practices:composer]: +Descargue e instale la biblioteca utilizando [Composer |best-practices:composer]: ```shell composer require nette/application ``` + +¿Por qué elegir Nette Application? .[#toc-why-choose-nette-application] +----------------------------------------------------------------------- + +Nette siempre ha sido pionera en tecnologías web. + +**Enrutador bidireccional:** Nette cuenta con un avanzado sistema de enrutamiento único en su bidireccionalidad - no sólo traduce URLs a acciones de la aplicación, sino que también puede generar URLs a la inversa. Esto significa que: +- Puede modificar la estructura de URL de toda la aplicación en cualquier momento sin modificar los archivos de plantilla. +- Las URL se canonizan automáticamente, lo que mejora el SEO. +- El enrutamiento se define en un solo lugar, no disperso en anotaciones + +**Componentes y señales:** El sistema de componentes integrado inspirado en Delphi y React.js es único entre los frameworks PHP: +- Permite crear elementos de interfaz de usuario reutilizables +- Soporta la composición jerárquica de componentes +- Ofrece un elegante manejo de peticiones AJAX mediante señales +- Amplia biblioteca de componentes listos para usar en [Componette](https://componette.org). + +**AJAX y Snippets:** Nette introdujo una forma revolucionaria de trabajar con AJAX en 2009, antes que soluciones como Hotwire para Ruby on Rails o Symfony UX Turbo: +- Los snippets permiten actualizar sólo partes de la página sin escribir JavaScript +- Integración automática con el sistema de componentes +- Invalidación inteligente de secciones de la página +- Transferencia mínima de datos + +**Plantillas Intuitivas [Latte |latte:]:** El sistema de plantillas más seguro para PHP con características avanzadas: +- Protección XSS automática con escape sensible al contexto +- Extensible con filtros personalizados, funciones y etiquetas +- Herencia de plantillas y fragmentos para AJAX +- Excelente soporte PHP 8.x con sistema de tipos + +**Inyección de dependencias:** Nette utiliza completamente la inyección de dependencias: +- Paso automático de dependencias (autowiring) +- Configuración utilizando el formato NEON +- Soporte para fábricas de componentes + + +Principales ventajas .[#toc-main-benefits] +------------------------------------------ + +- Seguridad**: Protección automática contra [vulnerabilidades |nette:vulnerability-protection] como XSS, CSRF, etc. +- Productividad**: Menos escritura, más funcionalidades gracias a un diseño inteligente +- Depuración**: [Depurador de Tracy |tracy:] con panel de enrutamiento +- Rendimiento**: Sistema de caché inteligente, carga perezosa de componentes. +- Flexibilidad**: Fácil modificación de la URL incluso después de finalizar la aplicación +- Componentes**: Sistema único de elementos de interfaz de usuario reutilizables +- Moderno**: Soporte completo para PHP 8.4+ y sistema de tipos + + +Primeros pasos .[#toc-getting-started] +-------------------------------------- + +1. Comprender [las aplicaciones |how-it-works] - Comprender la arquitectura básica +2. [Presentadores |presenters] - Trabajar con presentadores y acciones +3. [Plantillas |templates] - Creación de plantillas en Latte +4. [Enrutamiento |routing] - Configuración de URL +5. [Componentes interactivos |components] - Uso del sistema de componentes + + +Compatibilidad con PHP .[#toc-php-compatibility] +------------------------------------------------ + | versión | compatible con PHP |-----------|------------------- -| Aplicación Nette 4.0 | PHP 8.0 - 8.2 -| Aplicación Nette 3.1 | PHP 7.2 - 8.2 +| Aplicación Nette 4.0 | PHP 8.1 - 8.4 +| Aplicación Nette 3.2 | PHP 8.1 - 8.4 +| Aplicación Nette 3.1 | PHP 7.2 - 8.3 | Aplicación Nette 3.0 | PHP 7.1 - 8.0 | Aplicación Nette 2.4 | PHP 5.6 - 8.0 -Se aplica a las últimas versiones de parches. +Válido para las últimas versiones de parches. \ No newline at end of file diff --git a/application/es/@left-menu.texy b/application/es/@left-menu.texy index c615204682..be808de52f 100644 --- a/application/es/@left-menu.texy +++ b/application/es/@left-menu.texy @@ -4,7 +4,7 @@ Aplicación Nette - [Arranque |Bootstrap] - [Presentadores |Presenters] - [Plantillas |Templates] -- [Módulos |Modules] +- [Estructura del directorio |directory-structure] - [Enrutamiento |Routing] - [Creación de enlaces URL |creating-links] - [Componentes interactivos |components] diff --git a/application/es/ajax.texy b/application/es/ajax.texy index 0ba8886dd4..d1414e84c4 100644 --- a/application/es/ajax.texy +++ b/application/es/ajax.texy @@ -3,10 +3,10 @@ AJAX y fragmentos
    -Hoy en día, las aplicaciones web modernas se ejecutan mitad en un servidor y mitad en un navegador. AJAX es un factor de unión vital. ¿Qué soporte ofrece Nette Framework? -- envío de fragmentos de plantillas (los llamados *snippets*) +En la era de las aplicaciones web modernas, donde la funcionalidad a menudo se extiende entre el servidor y el navegador, AJAX es un elemento de conexión esencial. ¿Qué opciones ofrece Nette Framework en este ámbito? +- envío de partes de la plantilla, los llamados snippets - paso de variables entre PHP y JavaScript -- depuración de aplicaciones AJAX +- herramientas para depurar peticiones AJAX
    @@ -14,29 +14,32 @@ Hoy en día, las aplicaciones web modernas se ejecutan mitad en un servidor y mi Solicitud AJAX .[#toc-ajax-request] =================================== -Una petición AJAX no difiere de una petición clásica: se llama al presentador con una vista y unos parámetros específicos. También depende del presentador cómo responder a ella: puede utilizar su propia rutina, que devuelve un fragmento de código HTML (HTML snippet), un documento XML, un objeto JSON o código JavaScript. +Una petición AJAX fundamentalmente no difiere de una petición HTTP clásica. Se llama a un presentador con parámetros específicos. Depende del presentador cómo responder a la petición - puede devolver datos en formato JSON, enviar una parte de código HTML, un documento XML, etc. -En el lado del servidor, una petición AJAX puede detectarse utilizando el método de servicio [que encapsula la petición HTTP |http:request] `$httpRequest->isAjax()` (detecta basándose en la cabecera HTTP `X-Requested-With`). Dentro del presentador, se dispone de un acceso directo en forma del método `$this->isAjax()`. +En el navegador, iniciamos una petición AJAX utilizando la función `fetch()`: -Existe un objeto preprocesado llamado `payload` dedicado a enviar datos al navegador en JSON. - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // tratamiento de la respuesta +}); ``` -Para un control total sobre su salida JSON utilice el método `sendJson` en su presentador. Terminará el presentador inmediatamente y prescindirá de una plantilla: +En el lado del servidor, una petición AJAX es reconocida por el método `$httpRequest->isAjax()` del servicio [que encapsula la petición HTTP |http:request]. Utiliza la cabecera HTTP `X-Requested-With`, por lo que es imprescindible enviarla. Dentro del presentador, puede utilizar el método `$this->isAjax()`. + +Si desea enviar datos en formato JSON, utilice el método [`sendJson()` |presenters#Sending a response] método. El método también finaliza la actividad del presentador. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Si queremos enviar HTML, podemos establecer una plantilla especial para peticiones AJAX: +Si planea responder con una plantilla especial diseñada para AJAX, puede hacerlo de la siguiente manera: ```php public function handleClick($param): void @@ -44,27 +47,43 @@ public function handleClick($param): void if ($this->isAjax()) { $this->template->setFile('path/to/ajax.latte'); } - // ... + //... } ``` +Recortes .[#toc-snippets] +========================= + +La herramienta más potente que ofrece Nette para conectar el servidor con el cliente son los snippets. Con ellos, puedes convertir una aplicación ordinaria en una AJAX con el mínimo esfuerzo y unas pocas líneas de código. El ejemplo Fifteen demuestra cómo funciona todo, y su código puede encontrarse en [GitHub |https://github.com/nette-examples/fifteen]. + +Los snippets, o recortes, permiten actualizar sólo partes de la página, en lugar de recargarla entera. Esto es más rápido y eficiente, y también proporciona una experiencia de usuario más cómoda. Puede que los snippets te recuerden a Hotwire para Ruby on Rails o a Symfony UX Turbo. Curiosamente, Nette introdujo los snippets 14 años antes. + +¿Cómo funcionan los fragmentos? Cuando se carga la página por primera vez (una petición no-AJAX), se carga toda la página, incluidos todos los fragmentos. Cuando el usuario interactúa con la página (por ejemplo, hace clic en un botón, envía un formulario, etc.), en lugar de cargarse toda la página, se realiza una solicitud AJAX. El código del presentador ejecuta la acción y decide qué fragmentos deben actualizarse. Nette renderiza estos fragmentos y los envía en forma de matriz JSON. A continuación, el código de gestión del navegador vuelve a insertar en la página los fragmentos recibidos. Por lo tanto, sólo se transfiere el código de los fragmentos modificados, lo que ahorra ancho de banda y acelera la carga en comparación con la transferencia de todo el contenido de la página. + + Naja .[#toc-naja] -================= +----------------- -K obsluze AJAXových požadavků na straně prohlížeče slouží [knihovna Naja |https://naja.js.org], [instálalo |https://naja.js.org/#/guide/01-install-setup-naja] como un paquete node.js (para usarlo con Webpack, Rollup, Vite, Parcel y más): +Para manejar snippets en el lado del navegador, se utiliza la [librería Naja |https://naja.js.org]. [Instálala |https://naja.js.org/#/guide/01-install-setup-naja] como un paquete node.js (para usar con aplicaciones como Webpack, Rollup, Vite, Parcel, y otras): ```shell npm install naja ``` -...o insertarlo directamente en la plantilla de la página: +... o insértala directamente en la plantilla de la página: ```html ``` -Para crear una solicitud AJAX a partir de un enlace normal (señal) o el envío de un formulario, basta con marcar el enlace, formulario o botón correspondiente con la clase `ajax`: +Primero hay que [inicializar |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] la biblioteca: + +```js +naja.initialize(); +``` + +Para convertir un enlace ordinario (señal) o el envío de un formulario en una petición AJAX, basta con marcar el enlace, formulario o botón correspondiente con la clase `ajax`: ```html Go @@ -74,64 +93,39 @@ Para crear una solicitud AJAX a partir de un enlace normal (señal) o el envío or +
    ``` -Fragmentos .[#toc-snippets] -=========================== - -Existe una herramienta mucho más potente de soporte AJAX integrado: los snippets. Su uso permite convertir una aplicación normal en una aplicación AJAX utilizando sólo unas pocas líneas de código. Cómo funciona todo se demuestra en el ejemplo Fifteen cuyo código también está accesible en la compilación o en [GitHub |https://github.com/nette-examples/fifteen]. - -La forma en que funcionan los snippets es que toda la página se transfiere durante la petición inicial (es decir, no AJAX) y luego con cada [subpetición |components#signal] AJAX (petición de la misma vista del mismo presentador) sólo se transfiere el código de las partes modificadas en el repositorio `payload` mencionado anteriormente. - -Puede que los snippets te recuerden a Hotwire para Ruby on Rails o a Symfony UX Turbo, pero Nette los inventó catorce años antes. - +Redibujar fragmentos .[#toc-redrawing-snippets] +----------------------------------------------- -Invalidación de Snippets .[#toc-invalidation-of-snippets] -========================================================= - -Cada descendiente de la clase [Control |components] (que también es un Presentador) es capaz de recordar si hubo algún cambio durante una petición que requiera que se vuelva a renderizar. Hay un par de métodos para manejar esto: `redrawControl()` y `isControlInvalid()`. Un ejemplo: +Cada objeto de la clase [Control |components] (incluido el propio Presentador) mantiene un registro de si se han producido cambios que requieran su redibujado. Para ello se emplea el método `redrawControl()`. ```php public function handleLogin(string $user): void { - // The object has to re-render after the user has logged in + // después de iniciar la sesión, es necesario volver a dibujar la parte pertinente $this->redrawControl(); - // ... + //... } ``` -Nette, sin embargo, ofrece una resolución aún más fina que la de los componentes completos. Los métodos mencionados aceptan el nombre de un "fragmento" como parámetro opcional. Un "fragmento" es básicamente un elemento de su plantilla marcado para ese propósito por una tag Latte, más sobre esto más adelante. Así, es posible pedir a un componente que redibuje sólo *partes* de su plantilla. Si se invalida todo el componente, entonces se vuelven a renderizar todos sus fragmentos. Un componente es "inválido" también si cualquiera de sus subcomponentes es inválido. - -```php -$this->isControlInvalid(); // -> false -$this->redrawControl('header'); // invalidates the snippet named 'header' -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, at least one snippet is invalid +Nette también permite un control más preciso de lo que hay que redibujar. El método mencionado puede tomar el nombre del fragmento como argumento. Así, es posible invalidar (es decir: forzar un redibujado) a nivel de la parte de la plantilla. Si se invalida todo el componente, también se redibujan todos sus fragmentos: -$this->redrawControl(); // invalidates the whole component, every snippet -$this->isControlInvalid('footer'); // -> true +```php +// invalida el fragmento de cabecera +$this->redrawControl('header'); ``` -Un componente que recibe una señal se marca automáticamente para ser redibujado. - -Gracias al redibujado de fragmentos, sabemos exactamente qué partes de qué elementos deben redibujarse. - - -Etiqueta `{snippet} … {/snippet}` .{toc: Tag snippet} -===================================================== - -El renderizado de la página procede de forma muy similar a una petición normal: se cargan las mismas plantillas, etc. Sin embargo, lo esencial es dejar fuera las partes que no deben llegar a la salida; las demás partes se asociarán a un identificador y se enviarán al usuario en un formato comprensible para un manipulador JavaScript. - -Sintaxis .[#toc-syntax] ------------------------ +Fragmentos en Latte .[#toc-snippets-in-latte] +--------------------------------------------- -Si hay un control o un fragmento en la plantilla, tenemos que envolverlo usando la etiqueta `{snippet} ... {/snippet}` pair - se asegurará de que el fragmento renderizado será "recortado" y enviado al navegador. También lo encerrará en una etiqueta helper `
    ` (es posible utilizar otra). En el siguiente ejemplo se define un fragmento llamado `header`. También puede representar la plantilla de un componente: +Utilizar snippets en Latte es extremadamente fácil. Para definir una parte de la plantilla como fragmento, basta con envolverla en las etiquetas `{snippet}` y `{/snippet}`: ```latte {snippet header} @@ -139,7 +133,9 @@ Si hay un control o un fragmento en la plantilla, tenemos que envolverlo usando {/snippet} ``` -Si desea crear un fragmento con un elemento contenedor distinto de `
    ` o añadir atributos personalizados al elemento, puede utilizar la siguiente definición: +El fragmento crea un elemento `
    ` en la página HTML con un `id` especialmente generado. Al redibujar un fragmento, se actualiza el contenido de este elemento. Por lo tanto, cuando la página se renderiza inicialmente, también deben renderizarse todos los fragmentos, aunque inicialmente puedan estar vacíos. + +También puede crear un fragmento con un elemento distinto de `
    ` mediante un atributo n:attribute: ```latte
    @@ -148,138 +144,106 @@ Si desea crear un fragmento con un elemento contenedor distinto de `
    ` o añ ``` -Fragmentos dinámicos .[#toc-dynamic-snippets] -============================================= +Áreas de recortes .[#toc-snippet-areas] +--------------------------------------- -En Nette también puede definir fragmentos con un nombre dinámico basado en un parámetro de tiempo de ejecución. Esto es más adecuado para varias listas en las que necesitamos cambiar sólo una fila pero no queremos transferir toda la lista junto con ella. Un ejemplo de esto sería: +Los nombres de los recortes también pueden ser expresiones: ```latte -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Hay un fragmento estático llamado `itemsContainer`, que contiene varios fragmentos dinámicos: `item-0` `item-1` y así sucesivamente. +De este modo, obtendremos varios fragmentos como `item-0`, `item-1`, etc. Si invalidáramos directamente un fragmento dinámico (por ejemplo, `item-1`), no se volvería a dibujar nada. La razón es que los fragmentos funcionan como verdaderos extractos y sólo ellos mismos se renderizan directamente. Sin embargo, en la plantilla no existe técnicamente un fragmento llamado `item-1`. Sólo aparece cuando se ejecuta el código que rodea al fragmento, en este caso, el bucle foreach. Por lo tanto, marcaremos la parte de la plantilla que necesita ser ejecutada con la etiqueta `{snippetArea}`: -No puedes redibujar un fragmento dinámico directamente (redibujar `item-1` no tiene ningún efecto), tienes que redibujar su fragmento padre (en este ejemplo `itemsContainer`). Esto hace que se ejecute el código del fragmento padre, pero entonces sólo se envían al navegador sus sub fragmentos. Si desea enviar sólo uno de los sub fragmentos, debe modificar la entrada del fragmento padre para que no genere los otros sub fragmentos. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -En el ejemplo anterior tiene que asegurarse de que para una petición AJAX sólo se añadirá un elemento a la matriz `$list`, por lo que el bucle `foreach` sólo imprimirá un fragmento dinámico. +Y redibujaremos tanto el fragmento individual como toda el área general: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * This method returns data for the list. - * Usually this would just request the data from a model. - * For the purpose of this example, the data is hard-coded. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +También es esencial asegurarse de que la matriz `$items` contiene sólo los elementos que deben ser redibujados. -Fragmentos en una plantilla incluida .[#toc-snippets-in-an-included-template] -============================================================================= - -Puede ocurrir que el snippet esté en una plantilla que está siendo incluida desde una plantilla diferente. En ese caso necesitamos envolver el código de inclusión en la segunda plantilla con la tag `snippetArea`, entonces redibujamos tanto el snippetArea como el snippet real. - -La tag `snippetArea` garantiza que se ejecute el código que contiene, pero que sólo se envíe al navegador el fragmento real de la plantilla incluida. +Cuando se inserta otra plantilla en la principal utilizando la etiqueta `{include}`, que tiene fragmentos, es necesario envolver de nuevo la plantilla incluida en un `snippetArea` e invalidar conjuntamente el fragmento y el área: ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* included.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -También se puede combinar con fragmentos dinámicos. +Fragmentos en componentes .[#toc-snippets-in-components] +-------------------------------------------------------- -Añadir y eliminar .[#toc-adding-and-deleting] -============================================= - -Si añades un nuevo elemento a la lista e invalidas `itemsContainer`, la petición AJAX devuelve fragmentos que incluyen el nuevo, pero el manejador javascript no podrá renderizarlo. Esto se debe a que no hay ningún elemento HTML con el ID recién creado. - -En este caso, la forma más sencilla es envolver toda la lista en un fragmento más e invalidarlo todo: +Puede crear fragmentos dentro de los [componentes |components] y Nette los redibujará automáticamente. Sin embargo, hay una limitación específica: para redibujar fragmentos, llama al método `render()` sin ningún parámetro. Por lo tanto, pasar parámetros en la plantilla no funcionará: ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Envío de datos de usuario .[#toc-sending-user-data] +--------------------------------------------------- + +Junto con los fragmentos, puede enviar cualquier dato adicional al cliente. Simplemente escríbalos en el objeto `payload`: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + //... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -Lo mismo ocurre con la eliminación de un elemento. Sería posible enviar un fragmento vacío, pero normalmente las listas pueden paginarse y sería complicado implementar la eliminación de un elemento y la carga de otro (que solía estar en una página diferente de la lista paginada). - -Envío de parámetros al componente .[#toc-sending-parameters-to-component] -========================================================================= +Envío de parámetros .[#toc-sending-parameters] +============================================== Cuando enviamos parámetros al componente a través de una petición AJAX, ya sean parámetros de señal o parámetros persistentes, debemos proporcionar su nombre global, que también contiene el nombre del componente. El nombre completo del parámetro devuelve el método `getParameterId()`. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -Y manejar el método con s parámetros correspondientes en el componente. +Un método handle con los parámetros correspondientes en el componente: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/es/bootstrap.texy b/application/es/bootstrap.texy index 3b67d5c4b9..a1f0ff2657 100644 --- a/application/es/bootstrap.texy +++ b/application/es/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + private Configurator $configurator; + private string $rootDir; + + public function __construct() + { + $this->rootDir = dirname(__DIR__); + // El configurador se encarga de configurar el entorno y los servicios de la aplicación. + $this->configurator = new Configurator; + // Establecer el directorio para los archivos temporales generados por Nette (por ejemplo, plantillas compiladas). + $this->configurator->setTempDirectory($this->rootDir . '/temp'); + } + + public function bootWebApplication(): Nette\DI\Container + { + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); + } + + private function initializeEnvironment(): void { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // Nette es inteligente, y el modo de desarrollo se activa automáticamente, + // o puede habilitarlo para una dirección IP específica descomentando la siguiente línea: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Habilita Tracy: la última herramienta de depuración "navaja suiza". + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: carga automáticamente todas las clases en el directorio dado + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Carga archivos de configuración + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php .[#toc-index-php] =========================== -En el caso de las aplicaciones web, el archivo inicial es `index.php`, que se encuentra en el directorio público `www/`. Permite a la clase `Bootstrap` inicializar el entorno y devolver el `$configurator` que crea el contenedor DI. Luego obtiene el servicio `Application`, que ejecuta la aplicación web: +En el caso de las aplicaciones web, el archivo principal es `index.php`, que se encuentra en el [directorio público |directory-structure#public-directory-www] `www/`. Esto hará que la clase Bootstrap inicialice el entorno y produzca un contenedor DI. A continuación, obtiene de él el servicio `Application`, que inicia la aplicación web: ```php -// initialize the environment + get Configurator object -$configurator = App\Bootstrap::boot(); -// create a DI container -$container = $configurator->createContainer(); -// DI container creates a Nette\Application\Application object +$bootstrap = new App\Bootstrap; +// Inicializar el entorno + crear un contenedor DI +$container = $bootstrap->bootWebApplication(); +// El contenedor DI crea un objeto Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -// start Nette application +// Inicia la aplicación Nette y gestiona la petición entrante $application->run(); ``` @@ -59,26 +84,42 @@ Como puedes ver, la clase [api:Nette\Bootstrap\Configurator], que ahora presenta Modo Desarrollo vs Producción .[#toc-development-vs-production-mode] ==================================================================== -Nette distingue entre dos modos básicos en los que se ejecuta una petición: desarrollo y producción. El modo de desarrollo está enfocado a la máxima comodidad del programador, se visualiza Tracy, la caché se actualiza automáticamente al cambiar las plantillas o la configuración del contenedor DI, etc. El modo de producción está enfocado al rendimiento, Tracy sólo registra los errores y no se comprueban los cambios de plantillas y otros ficheros. +Nette se comporta de forma diferente dependiendo de si se está ejecutando en un servidor de desarrollo o de producción: + +🛠️ Modo Desarrollo: + - Muestra la barra de depuración de Tracy con información útil (por ejemplo, consultas SQL, tiempo de ejecución, uso de memoria). + - Muestra una página de error detallada con trazas de llamadas a funciones y contenidos de variables cuando se produce un error. + - Actualiza automáticamente la caché cuando se modifican las plantillas Latte, los archivos de configuración, etc. + + +Modo de producción: + - No muestra ninguna información de depuración; se registran todos los errores. + - Muestra un `ErrorPresenter` o una página genérica de "Error de servidor" cuando se produce un error. + - ¡La caché nunca se actualiza automáticamente! + - Optimizado para velocidad y seguridad. -La selección del modo se hace por autodetección, por lo que normalmente no hay necesidad de configurar o cambiar nada manualmente. El modo es desarrollo si la aplicación se ejecuta en localhost (es decir, la dirección IP `127.0.0.1` o `::1`) y no hay proxy presente (es decir, su cabecera HTTP). De lo contrario, se ejecuta en modo de producción. + +El modo se determina automáticamente, por lo que en la mayoría de los casos no es necesario configurarlo o cambiarlo manualmente: + +- Modo de desarrollo: Activo en localhost (dirección IP `127.0.0.1` o `::1`) a menos que se esté utilizando un proxy (es decir, basándose en sus cabeceras HTTP). +- Modo de producción: Activo en cualquier otro lugar. Si desea habilitar el modo de desarrollo en otros casos, por ejemplo, para los programadores que acceden desde una dirección IP específica, puede utilizar `setDebugMode()`: ```php -$configurator->setDebugMode('23.75.345.200'); // one or more IP addresses +$this->configurator->setDebugMode('23.75.345.200'); // one or more IP addresses ``` Recomendamos encarecidamente combinar una dirección IP con una cookie. Almacenaremos un token secreto en la cookie `nette-debug`, por ejemplo `secret1234`, y el modo de desarrollo se activará para los programadores con esta combinación de IP y cookie. ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` También podemos desactivar completamente el modo de desarrollo, incluso para localhost: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` Nótese que el valor `true` activa el modo desarrollador por fuerza, lo que nunca debería ocurrir en un servidor de producción. @@ -90,7 +131,7 @@ Herramienta de depuración Tracy .[#toc-debugging-tool-tracy] Para facilitar la depuración, activaremos la gran herramienta [Tracy |tracy:]. En modo desarrollador visualiza los errores y en modo producción los registra en el directorio especificado: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ Archivos temporales .[#toc-temporary-files] Nette utiliza la caché para el contenedor DI, RobotLoader, plantillas, etc. Por lo tanto es necesario establecer la ruta al directorio donde se almacenará la caché: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` En Linux o macOS, establezca los [permisos de escritura |nette:troubleshooting#Setting directory permissions] para los directorios `log/` y `temp/`. @@ -112,7 +153,7 @@ RobotLoader .[#toc-robotloader] Normalmente, querremos cargar automáticamente las clases usando [RobotLoader |robot-loader:], así que tenemos que iniciarlo y dejar que cargue las clases desde el directorio donde se encuentra `Bootstrap.php` (es decir, `__DIR__`) y todos sus subdirectorios: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ Zona horaria .[#toc-timezone] Configurator le permite especificar una zona horaria para su aplicación. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ En el modo de desarrollo, el contenedor se actualiza automáticamente cada vez q Los archivos de configuración se cargan utilizando `addConfig()`: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` El método `addConfig()` se puede llamar varias veces para añadir varios archivos. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ Parámetros estáticos .[#toc-static-parameters] Los parámetros utilizados en los archivos de configuración pueden definirse [en la sección `parameters` |dependency-injection:configuration#parameters] y también pasarse (o sobrescribirse) por el método `addStaticParameters()` (tiene el alias `addParameters()`). Es importante que los diferentes valores de los parámetros provoquen la generación de contenedores DI adicionales, es decir, clases adicionales. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ Parámetros dinámicos .[#toc-dynamic-parameters] También podemos añadir parámetros dinámicos al contenedor, sus diferentes valores, a diferencia de los parámetros estáticos, no provocarán la generación de nuevos contenedores DI. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ Las variables de entorno podrían estar fácilmente disponibles utilizando parámetros dinámicos. Podemos acceder a ellas a través de `%env.variable%` en archivos de configuración. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ Puede utilizar los siguientes parámetros estáticos en los archivos de configur - `%wwwDir%` es la ruta absoluta al directorio que contiene el archivo de entrada `index.php` - `%tempDir%` es la ruta absoluta al directorio para los archivos temporales - `%vendorDir%` es la ruta absoluta al directorio donde Composer instala las bibliotecas +- `%rootDir%` es la ruta absoluta al directorio raíz del proyecto - `%debugMode%` indica si la aplicación está en modo depuración - `%consoleMode%` indica si la solicitud llegó a través de la línea de comandos @@ -225,7 +268,7 @@ services: Creamos una nueva instancia y la insertamos en bootstrap: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Diferentes entornos .[#toc-different-environments] ================================================== -Siéntete libre de personalizar la clase `Bootstrap` para adaptarla a tus necesidades. Puedes añadir parámetros al método `boot()` para diferenciar proyectos web, o añadir otros métodos, como `bootForTests()`, que inicializa el entorno para pruebas unitarias, `bootForCli()` para scripts llamados desde la línea de comandos, etc. +No dude en personalizar la clase `Bootstrap` según sus necesidades. Puedes añadir parámetros al método `bootWebApplication()` para diferenciar entre proyectos web. Alternativamente, puedes añadir otros métodos, como `bootTestEnvironment()` para inicializar el entorno para pruebas unitarias, `bootConsoleApplication()` para scripts llamados desde la línea de comandos, etc. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container +{ + Tester\Environment::setup(); // Inicialización del comprobador de redes + $this->setupContainer(); + return $this->configurator->createContainer(); +} + +public function bootConsoleApplication(): Nette\DI\Container { - $configurator = self::boot(); - Tester\Environment::setup(); // Nette Tester initialization - return $configurator; + $this->configurator->setDebugMode(false); + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); } ``` diff --git a/application/es/components.texy b/application/es/components.texy index 7f736ea960..5e58b7e920 100644 --- a/application/es/components.texy +++ b/application/es/components.texy @@ -230,6 +230,28 @@ En la plantilla, estos mensajes están disponibles en la variable `$flashes` com ``` +Redirección tras una señal .[#toc-redirection-after-a-signal] +============================================================= + +Después de procesar una señal de componente, a menudo se produce una redirección. Esta situación es similar a la de los formularios: después de enviar un formulario, también redirigimos para evitar que se vuelvan a enviar los datos cuando se actualiza la página en el navegador. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Dado que un componente es un elemento reutilizable y, por lo general, no debería tener una dependencia directa de presentadores específicos, los métodos `redirect()` y `link()` interpretan automáticamente el parámetro como una señal de componente: + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +Si necesita redirigir a un presentador o acción diferente, puede hacerlo a través del presentador: + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Parámetros persistentes .[#toc-persistent-parameters] ===================================================== @@ -347,7 +369,7 @@ services: Por último, vamos a utilizar esta fábrica en nuestro presentador: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ Componentes en profundidad .[#toc-components-in-depth] Los componentes en una aplicación Nette son las partes reutilizables de una aplicación web que incrustamos en las páginas, que es el tema de este capítulo. ¿Cuáles son exactamente las capacidades de un componente? 1) es renderizable en una plantilla -2) sabe qué parte de sí mismo renderizar durante una [petición AJAX |ajax#invalidation] (fragmentos) +2) sabe [qué parte de sí mismo |ajax#snippets] debe representar durante una petición AJAX (fragmentos) 3) tiene la capacidad de almacenar su estado en una URL (parámetros persistentes) 4) tiene la capacidad de responder a las acciones del usuario (señales) 5) crea una estructura jerárquica (donde la raíz es el presentador) diff --git a/application/es/configuration.texy b/application/es/configuration.texy index 5dfa60ab36..bf714ccf61 100644 --- a/application/es/configuration.texy +++ b/application/es/configuration.texy @@ -14,10 +14,14 @@ application: debugger: ... # (bool) por defecto true # ¿se llamará al presentador de errores en caso de error? - catchExceptions: ... # (bool) por defecto a true en modo producción + # sólo tiene efecto en modo desarrollador + catchExceptions: ... # (bool) por defecto true # nombre del presentador de errores - errorPresenter: Error # (string) por defecto 'Nette:Error' + errorPresenter: Error # (string|array) por defecto 'Nette:Error' + + # define alias para presentadores y eventos + aliases: ... # define las reglas para resolver el nombre del presentador a una clase mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) por defecto false ``` -Debido a que los presentadores de errores no son llamados por defecto en modo desarrollo y los errores son mostrados por Tracy, cambiar el valor `catchExceptions` a `true` ayuda a verificar que los presentadores de errores funcionan correctamente durante el desarrollo. +A partir de la versión 3.2 de `nette/application` es posible definir un par de presentadores de errores: + +```neon +application: + errorPresenter: + 4xx: Error4xx # para Nette\Application\BadRequestException + 5xx: Error5xx # para otras excepciones +``` La opción `silentLinks` determina cómo se comporta Nette en modo desarrollo cuando falla la generación de enlaces (por ejemplo, porque no hay presentador, etc). El valor por defecto `false` significa que Nette activa `E_USER_WARNING`. El valor `true` suprime este mensaje de error. En un entorno de producción, siempre se invoca `E_USER_WARNING`. También podemos influir en este comportamiento configurando la variable del presentador [$invalidLinkMode |creating-links#Invalid Links]. -El [mapeo define las reglas |modules#mapping] por las cuales el nombre de la clase se deriva del nombre del presentador. +[Los alias simplifican las referencias a |creating-links#aliases] los presentadores más utilizados. + +El [mapeo define las reglas |directory-structure#Presenter Mapping] por las que el nombre de la clase se deriva del nombre del presentador. Registro automático de presentadores .[#toc-automatic-registration-of-presenters] @@ -82,6 +95,9 @@ latte: # habilita la [comprobación del código generado |latte:develop#Checking Generated Code] phpLinter: ... # (string) por defecto es null + # establece la configuración regional + locale: cs_CZ # (string) por defecto es null + # clase de $this->plantilla templateClass: App\MyTemplateClass # por defecto Nette\Bridges\ApplicationLatte\DefaultTemplate ``` @@ -91,7 +107,7 @@ Si está utilizando la versión 3 de Latte, puede añadir una nueva [extensión ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` /--comment diff --git a/application/es/creating-links.texy b/application/es/creating-links.texy index cd4645f3a5..9bd6cd66cc 100644 --- a/application/es/creating-links.texy +++ b/application/es/creating-links.texy @@ -38,7 +38,7 @@ También es posible pasar parámetros con nombre. El siguiente enlace pasa el pa detail ``` -Si el método `ProductPresenter::renderShow()` no tiene `$lang` en su firma, puede leer el valor del parámetro usando `$lang = $this->getParameter('lang')`. +Si el método `ProductPresenter::renderShow()` no tiene `$lang` en su firma, puede recuperar el valor del parámetro utilizando `$lang = $this->getParameter('lang')` o desde la [propiedad |presenters#Request Parameters]. Si los parámetros se almacenan en una matriz, pueden expandirse con el operador `...` (o `(expand)` en Latte 2.x): @@ -103,7 +103,7 @@ Si la acción es `default`, podemos omitirlo, pero los dos puntos deben permanec home ``` -Los enlaces también pueden apuntar a otros [módulos |modules]. Aquí, los enlaces se distinguen en relativos a los submódulos, o absolutos. El principio es análogo a las rutas de disco, sólo que en lugar de barras inclinadas hay dos puntos. Supongamos que el presentador real forma parte del módulo `Front`, entonces escribiremos: +Los enlaces también pueden apuntar a otros [módulos |directory-structure#Presenters and Templates]. Aquí, los enlaces se distinguen en relativos a los submódulos, o absolutos. El principio es análogo a las rutas de disco, sólo que en lugar de barras inclinadas hay dos puntos. Supongamos que el presentador actual forma parte del módulo `Front`, entonces escribiremos: ```latte link to Front:Shop:Product:show @@ -140,7 +140,7 @@ El destino `this` creará un enlace a la página actual: refresh ``` -Al mismo tiempo, todos los parámetros especificados en la firma de la directiva `render()` o `action()` se transfieren. Así, si estamos en las páginas `Product:show` y `id:123`, el enlace a `this` también pasará este parámetro. +Al mismo tiempo, todos los parámetros especificados en la firma de la directiva `action()` o `render()` si el método `action()` no está definido, se transfieren. Así, si estamos en las páginas `Product:show` y `id:123`, el enlace a `this` también pasará este parámetro. Por supuesto, es posible especificar los parámetros directamente: @@ -213,7 +213,7 @@ Dado que los [componentes |components] son unidades reutilizables independientes Si queremos enlazar con presentadores en la plantilla de componentes, utilizaremos la etiqueta `{plink}`: ```latte -home +home ``` o en el código @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Alias .[#toc-aliases]{data-version:v3.2.2} +========================================== + +A veces resulta útil asignar un alias fácil de recordar a un par Presentador:acción. Por ejemplo, puede nombrar la página de inicio `Front:Home:default` simplemente como `home` o `Admin:Dashboard:default` como `admin`. + +Los alias se definen en la [configuración |configuration] con la clave `application › aliases`: + +```neon +application: + aliases: + home: Front:Home:default + admin: Admin:Dashboard:default + sign: Front:Sign:in +``` + +En los enlaces, se escriben utilizando el símbolo arroba, por ejemplo: + +```latte +administration +``` + +Se admiten en todos los métodos que trabajan con enlaces, como `redirect()` y similares. + + Enlaces no válidos .[#toc-invalid-links] ======================================== @@ -257,6 +281,6 @@ Generador de enlaces .[#toc-linkgenerator] LinkGenerator es un servicio que puede haber pasado por el constructor y luego crear enlaces utilizando su método `link()`. -Hay una diferencia en comparación con los presentadores. LinkGenerator crea todos los enlaces como URLs absolutas. Además, no existe un "presentador actual", por lo que no es posible especificar únicamente el nombre de la acción `link('default')` o las rutas relativas a los [módulos |modules]. +En comparación con los presentadores, hay una diferencia. LinkGenerator crea todos los enlaces directamente como URLs absolutas. Además, no existe un "presentador real", por lo que no puede simplemente listar el nombre de la acción `link('default')` como destino o listar rutas relativas a módulos. Los enlaces no válidos siempre lanzan `Nette\Application\UI\InvalidLinkException`. diff --git a/application/es/directory-structure.texy b/application/es/directory-structure.texy new file mode 100644 index 0000000000..5de26ff437 --- /dev/null +++ b/application/es/directory-structure.texy @@ -0,0 +1,526 @@ +Estructura de directorios de la aplicación +****************************************** + +
    + +¿Cómo diseñar una estructura de directorios clara y escalable para proyectos en Nette Framework? Te mostraremos prácticas probadas que te ayudarán a organizar tu código. Aprenderás: + +- cómo **estructurar lógicamente** la aplicación en directorios +- cómo diseñar la estructura para **escalar bien** a medida que crece el proyecto +- cuáles son las **posibles alternativas** y sus ventajas o desventajas + +
    + + +Es importante mencionar que Nette Framework no insiste en ninguna estructura específica. Está diseñado para adaptarse fácilmente a cualquier necesidad y preferencia. + + +Estructura básica del proyecto .[#toc-basic-project-structure] +============================================================== + +Aunque Nette Framework no dicta ninguna estructura de directorios fija, existe una disposición por defecto probada en forma de [Proyecto Web |https://github.com/nette/web-project]: + +/--pre +web-project/ +├── app/ ← directorio de la aplicación +├── assets/ ← SCSS, archivos JS, imágenes..., alternativamente resources/ +├── bin/ ← scripts de línea de comandos +├── config/ ← configuración +├── log/ ← errores registrados +├── temp/ ← archivos temporales, caché +├── tests/ ← pruebas +├── vendor/ ← bibliotecas instaladas por Composer +└── www/ ← directorio público (document-root) +\-- + +Puede modificar libremente esta estructura según sus necesidades - renombrar o mover carpetas. Entonces sólo tiene que ajustar las rutas relativas a los directorios en `Bootstrap.php` y posiblemente `composer.json`. No se necesita nada más, ni una reconfiguración compleja, ni cambios constantes. Nette tiene una autodetección inteligente y reconoce automáticamente la ubicación de la aplicación incluyendo su URL base. + + +Principios de organización del código .[#toc-code-organization-principles] +========================================================================== + +Cuando exploras por primera vez un nuevo proyecto, deberías ser capaz de orientarte rápidamente. Imagina que haces clic en el directorio `app/Model/` y ves esta estructura: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +De esto, sólo aprenderás que el proyecto utiliza algunos servicios, repositorios y entidades. No aprenderás nada sobre el propósito real de la aplicación. + +Veamos un enfoque diferente - **organización por dominios**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Esto es diferente - a primera vista está claro que se trata de un sitio de comercio electrónico. Los propios nombres de los directorios revelan lo que puede hacer la aplicación: trabaja con pagos, pedidos y productos. + +El primer enfoque (organización por tipo de clase) trae varios problemas en la práctica: el código que está lógicamente relacionado está disperso en diferentes carpetas y hay que saltar entre ellas. Por lo tanto, organizaremos por dominios. + + +Espacios de nombres .[#toc-namespaces] +-------------------------------------- + +Es convencional que la estructura de directorios se corresponda con los espacios de nombres de la aplicación. Esto significa que la ubicación física de los archivos se corresponde con su espacio de nombres. Por ejemplo, una clase ubicada en `app/Model/Product/ProductRepository.php` debería tener el espacio de nombres `App\Model\Product`. Este principio ayuda a orientar el código y simplifica la carga automática. + + +Singular y plural en los nombres .[#toc-singular-vs-plural-in-names] +-------------------------------------------------------------------- + +Observe que utilizamos el singular para los principales directorios de la aplicación: `app`, `config`, `log`, `temp`, `www`. Lo mismo ocurre dentro de la aplicación: `Model`, `Core`, `Presentation`. Esto se debe a que cada uno representa un concepto unificado. + +Del mismo modo, `app/Model/Product` representa todo lo relacionado con los productos. No lo llamamos `Products` porque no es una carpeta llena de productos (que contendría archivos como `iphone.php`, `samsung.php`). Es un espacio de nombres que contiene clases para trabajar con productos - `ProductRepository.php`, `ProductService.php`. + +La carpeta `app/Tasks` es plural porque contiene un conjunto de scripts ejecutables independientes - `CleanupTask.php`, `ImportTask.php`. Cada uno de ellos es una unidad independiente. + +Por coherencia, se recomienda utilizar: +- Singular para los espacios de nombres que representen una unidad funcional (aunque se trabaje con varias entidades). +- Plural para las colecciones de unidades independientes +- En caso de incertidumbre o si no quiere pensar en ello, elija singular + + +Directorio público `www/` .[#toc-public-directory-www] +====================================================== + +Este directorio es el único accesible desde la web (llamado document-root). A menudo puede encontrar el nombre `public/` en lugar de `www/` - es sólo una cuestión de convención y no afecta a la funcionalidad. El directorio contiene: +- [Punto de entrada de |bootstrap#index.php] la aplicación `index.php` +- Archivo `.htaccess` con reglas mod_rewrite (para Apache) +- Archivos estáticos (CSS, JavaScript, imágenes) +- Archivos subidos + +Para una correcta seguridad de la aplicación, es crucial tener correctamente [configurado el document-root |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url]. + +.[note] +Nunca coloques la carpeta `node_modules/` en este directorio - contiene miles de archivos que pueden ser ejecutables y no deberían ser accesibles públicamente. + + +Directorio de aplicaciones `app/` .[#toc-application-directory-app] +=================================================================== + +Este es el directorio principal con el código de la aplicación. Estructura básica: + +/--pre +app/ +├── Core/ ← cuestiones de infraestructura +├── Model/ ← lógica empresarial +├── Presentation/ ← presentadores y plantillas +├── Tasks/ ← scripts de comandos +└── Bootstrap.php ← clase de arranque de la aplicación +\-- + +`Bootstrap.php` es la [clase de inicio de la aplicación |bootstrap] que inicializa el entorno, carga la configuración y crea el contenedor DI. + +Veamos ahora en detalle los subdirectorios individuales. + + +Presentadores y plantillas .[#toc-presenters-and-templates] +=========================================================== + +Tenemos la parte de presentación de la aplicación en el directorio `app/Presentation`. Una alternativa es el corto `app/UI`. Este es el lugar para todos los presentadores, sus plantillas y cualquier clase de ayuda. + +Organizamos esta capa por dominios. En un proyecto complejo que combine comercio electrónico, blog y API, la estructura tendría este aspecto: + +/--pre +app/Presentation/ +├── Shop/ ← frontend de comercio electrónico +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← blog +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administración +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← puntos finales API + └── V1/ +\-- + +Por el contrario, para un blog sencillo utilizaríamos esta estructura: + +/--pre +app/Presentation/ +├── Front/ ← frontend del sitio web +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administración +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, sitemaps, etc. +\-- + +Carpetas como `Home/` o `Dashboard/` contienen presentadores y plantillas. Las carpetas como `Front/`, `Admin/` o `Api/` se denominan **módulos**. Técnicamente, se trata de directorios normales que sirven para la organización lógica de la aplicación. + +Cada carpeta con un presentador contiene un presentador de nombre similar y sus plantillas. Por ejemplo, la carpeta `Dashboard/` contiene: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← presentador +└── default.latte ← plantilla +\-- + +Esta estructura de directorios se refleja en los espacios de nombres de las clases. Por ejemplo, `DashboardPresenter` se encuentra en el espacio de nombres `App\Presentation\Admin\Dashboard` (véase [la asignación de presentadores |#presenter mapping]): + +```php +namespace App\Presentation\Admin\Dashboard; + +class DashboardPresenter extends Nette\Application\UI\Presenter +{ + //... +} +``` + +Nos referimos al presentador `Dashboard` dentro del módulo `Admin` en la aplicación utilizando la notación de dos puntos como `Admin:Dashboard`. A su acción `default` entonces como `Admin:Dashboard:default`. Para los módulos anidados, utilizamos más dos puntos, por ejemplo `Shop:Order:Detail:default`. + + +Desarrollo de estructuras flexibles .[#toc-flexible-structure-development] +-------------------------------------------------------------------------- + +Una de las grandes ventajas de esta estructura es lo elegantemente que se adapta a las crecientes necesidades del proyecto. Como ejemplo, tomemos la parte de generación de feeds XML. Inicialmente, tenemos un simple formulario: + +/--pre +Export/ +├── ExportPresenter.php ← un presentador para todas las exportaciones +├── sitemap.latte ← plantilla para el mapa del sitio +└── feed.latte ← plantilla para RSS feed +\-- + +Con el tiempo, se añaden más tipos de feeds y necesitamos más lógica para ellos... No hay problema. La carpeta `Export/` simplemente se convierte en un módulo: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← feed para Amazon + └── ebay.latte ← feed para eBay +\-- + +Esta transformación es completamente fluida: basta con crear nuevas subcarpetas, dividir el código en ellas y actualizar los enlaces (por ejemplo, de `Export:feed` a `Export:Feed:amazon`). Gracias a esto, podemos ampliar gradualmente la estructura según sea necesario, el nivel de anidamiento no está limitado en modo alguno. + +Por ejemplo, si en la administración tiene muchos presentadores relacionados con la gestión de pedidos, como `OrderDetail`, `OrderEdit`, `OrderDispatch` etc., puede crear un módulo (carpeta) `Order` para una mejor organización, que contendrá (carpetas para) los presentadores `Detail`, `Edit`, `Dispatch` y otros. + + +Ubicación de la plantilla .[#toc-template-location] +--------------------------------------------------- + +En los ejemplos anteriores, vimos que las plantillas se ubican directamente en la carpeta con el presentador: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← presentador +├── DashboardTemplate.php ← clase de plantilla opcional +└── default.latte ← plantilla +\-- + +Esta ubicación resulta ser la más conveniente en la práctica - usted tiene todos los archivos relacionados a la mano. + +Otra posibilidad es colocar las plantillas en una subcarpeta de `templates/`. Nette admite ambas variantes. Incluso puede colocar las plantillas completamente fuera de la carpeta `Presentation/`. Encontrará toda la información sobre las opciones de ubicación de las plantillas en el capítulo [Búsqueda de plantillas |templates#Template Lookup]. + + +Clases de ayuda y componentes .[#toc-helper-classes-and-components] +------------------------------------------------------------------- + +Los presentadores y las plantillas suelen venir acompañados de otros archivos de ayuda. Los colocamos lógicamente según su ámbito de aplicación: + +1. **Directamente con el presentador** en caso de componentes específicos para el presentador dado: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← componente para el listado de productos +└── FilterForm.php ← formulario de filtrado +\-- + +2. **Para el módulo** - se recomienda utilizar la carpeta `Accessory`, que se coloca ordenadamente al principio del alfabeto: + +/--pre +Front/ +├── Accessory/ +│ ├── NavbarControl.php ← componentes para frontend +│ └── TemplateFilters.php +├── Product/ +└── Cart/ +\-- + +3. **Para toda la aplicación** - en `Presentation/Accessory/`: +/--pre +app/Presentation/ +├── Accessory/ +│ ├── LatteExtension.php +│ └── TemplateFilters.php +├── Front/ +└── Admin/ +\-- + +O puede colocar clases de ayuda como `LatteExtension.php` o `TemplateFilters.php` en la carpeta de infraestructura `app/Core/Latte/`. Y los componentes en `app/Components`. La elección depende de las convenciones del equipo. + + +Modelo - Corazón de la aplicación .[#toc-model-heart-of-the-application] +======================================================================== + +El modelo contiene toda la lógica de negocio de la aplicación. Para su organización, se aplica la misma regla - estructuramos por dominios: + +/--pre +app/Model/ +├── Payment/ ← todo sobre los pagos +│ ├── PaymentFacade.php ← punto de entrada principal +│ ├── PaymentRepository.php +│ ├── Payment.php ← entidad +├── Order/ ← todo sobre pedidos +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← todo sobre envíos +\-- + +En el modelo, normalmente se encuentran estos tipos de clases: + +**Facades**: representan el principal punto de entrada a un dominio específico en la aplicación. Actúan como un orquestador que coordina la cooperación entre diferentes servicios para implementar casos de uso completos (como "crear pedido" o "procesar pago"). Bajo su capa de orquestación, la fachada oculta los detalles de implementación al resto de la aplicación, proporcionando así una interfaz limpia para trabajar con el dominio en cuestión. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // validación + // creación de pedidos + // envío de correos electrónicos + // estadísticas + } +} +``` + +**Servicios**: se centran en operaciones de negocio específicas dentro de un dominio. A diferencia de las fachadas, que orquestan casos de uso completos, un servicio implementa una lógica de negocio específica (como el cálculo de precios o el procesamiento de pagos). Los servicios suelen carecer de estado y pueden ser utilizados por las fachadas como bloques de construcción para operaciones más complejas, o directamente por otras partes de la aplicación para tareas más sencillas. + +```php +class PricingService +{ + public function calculateTotal(Order $order): Money + { + // cálculo del precio + } +} +``` + +**Repositorios**: gestionan toda la comunicación con el almacenamiento de datos, normalmente una base de datos. Su tarea es cargar y guardar entidades e implementar métodos para buscarlas. Un repositorio protege al resto de la aplicación de los detalles de implementación de la base de datos y proporciona una interfaz orientada a objetos para trabajar con los datos. + +```php +class OrderRepository +{ + public function find(int $id): ?Order + { + } + + public function findByCustomer(int $customerId): array + { + } +} +``` + +**Entidades**: objetos que representan los principales conceptos de negocio de la aplicación, que tienen su identidad y cambian con el tiempo. Suelen ser clases asignadas a tablas de bases de datos mediante ORM (como Nette Database Explorer o Doctrine). Las entidades pueden contener reglas de negocio relativas a sus datos y lógica de validación. + +```php +// Entidad asignada a la tabla de base de datos de pedidos +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, + ]); + } +} +``` + +**Objetos valor**: objetos inmutables que representan valores sin identidad propia - por ejemplo, una cantidad de dinero o una dirección de correo electrónico. Dos instancias de un objeto valor con los mismos valores se consideran idénticas. + + +Código de infraestructura .[#toc-infrastructure-code] +===================================================== + +La carpeta `Core/` (o también `Infrastructure/`) alberga la base técnica de la aplicación. El código de infraestructura suele incluir: + +/--pre +app/Core/ +├── Router/ ← enrutamiento y gestión de URL +│ └── RouterFactory.php +├── Security/ ← autenticación y autorización +│ ├── Authenticator.php +│ └── Authorizator.php +├── Logging/ ← registro y supervisión +│ ├── SentryLogger.php +│ └── FileLogger.php +├── Cache/ ← capa de caché +│ └── FullPageCache.php +└── Integration/ ← integración con servicios externos + ├── Slack/ + └── Stripe/ +\-- + +Para proyectos más pequeños, una estructura plana es naturalmente suficiente: + +/--pre +Core/ +├── RouterFactory.php +├── Authenticator.php +└── QueueMailer.php +\-- + +Esto es código que: + +- Gestiona la infraestructura técnica (enrutamiento, registro, almacenamiento en caché) +- Integra servicios externos (Sentry, Elasticsearch, Redis) +- Proporciona servicios básicos para toda la aplicación (correo, base de datos) +- Es en su mayor parte independiente del dominio específico - la caché o el logger funcionan igual para el comercio electrónico o el blog. + +¿Te preguntas si una determinada clase debe estar aquí o en el modelo? La diferencia clave es que el código en `Core/`: + +- No sabe nada sobre el dominio (productos, pedidos, artículos) +- Normalmente puede transferirse a otro proyecto +- Resuelve "cómo funciona" (cómo enviar correo), no "qué hace" (qué correo enviar) + +Ejemplo para una mejor comprensión: + +- `App\Core\MailerFactory` - crea instancias de la clase de envío de correo electrónico, gestiona la configuración SMTP +- `App\Model\OrderMailer` - utiliza `MailerFactory` para enviar correos sobre pedidos, conoce sus plantillas y cuándo deben enviarse + + +Scripts de comandos .[#toc-command-scripts] +=========================================== + +Las aplicaciones a menudo necesitan realizar tareas fuera de las peticiones HTTP habituales, ya sea procesamiento de datos en segundo plano, mantenimiento o tareas periódicas. Los scripts simples en el directorio `bin/` se utilizan para la ejecución, mientras que la lógica de implementación real se coloca en `app/Tasks/` (o `app/Commands/`). + +Ejemplo: + +/--pre +app/Tasks/ +├── Maintenance/ ← scripts de mantenimiento +│ ├── CleanupCommand.php ← eliminación de datos antiguos +│ └── DbOptimizeCommand.php ← optimización de la base de datos +├── Integration/ ← integración con sistemas externos +│ ├── ImportProducts.php ← importación desde el sistema del proveedor +│ └── SyncOrders.php ← sincronización de pedidos +└── Scheduled/ ← tareas periódicas + ├── NewsletterCommand.php ← envío de boletines + └── ReminderCommand.php ← notificaciones a clientes +\-- + +¿Qué pertenece al modelo y qué a las secuencias de comandos? Por ejemplo, la lógica para enviar un correo electrónico forma parte del modelo, el envío masivo de miles de correos electrónicos pertenece a `Tasks/`. + +Las tareas suelen [ejecutarse desde la línea de comandos |https://blog.nette.org/en/cli-scripts-in-nette-application] o mediante cron. También pueden ejecutarse a través de una petición HTTP, pero hay que tener en cuenta la seguridad. El presentador que ejecuta la tarea debe estar protegido, por ejemplo, sólo para usuarios registrados o con un token seguro y acceso desde direcciones IP permitidas. Para tareas largas, es necesario aumentar el límite de tiempo del script y utilizar `session_write_close()` para evitar el bloqueo de la sesión. + + +Otros directorios posibles .[#toc-other-possible-directories] +============================================================= + +Además de los directorios básicos mencionados, puedes añadir otras carpetas especializadas según las necesidades del proyecto. Veamos las más comunes y su uso: + +/--pre +app/ +├── Api/ ← lógica de la API independiente de la capa de presentación +├── Database/ ← scripts de migración y sembradores de datos de prueba +├── Components/ ← componentes visuales compartidos en toda la aplicación +├── Event/ ← útil si se utiliza una arquitectura basada en eventos +├── Mail/ ← plantillas de correo electrónico y lógica relacionada +└── Utils/ ← clases de ayuda +\-- + +Para los componentes visuales compartidos que se utilizan en los presentadores de toda la aplicación, puedes utilizar la carpeta `app/Components` o `app/Controls`: + +/--pre +app/Components/ +├── Form/ ← componentes de formulario compartidos +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← componentes para listados de datos +│ └── DataGrid.php +└── Navigation/ ← elementos de navegación + ├── Breadcrumbs.php + └── Menu.php +\-- + +Aquí es donde pertenecen los componentes con una lógica más compleja. Si quieres compartir componentes entre varios proyectos, es bueno separarlos en un paquete compositor independiente. + +En el directorio `app/Mail` puedes colocar la gestión de la comunicación por correo electrónico: + +/--pre +app/Mail/ +├── templates/ ← plantillas de correo electrónico +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Mapa del presentador .[#toc-presenter-mapping] +============================================== + +El mapeo define reglas para derivar nombres de clase a partir de nombres de presentador. Las especificamos en la [configuración |configuration] bajo la clave `application › mapping`. + +En esta página, hemos mostrado que colocamos los presentadores en la carpeta `app/Presentation` (o `app/UI`). Debemos informar a Nette de esta convención en el archivo de configuración. Una línea es suficiente: + +```neon +application: + mapping: App\Presentation\*\**Presenter +``` + +¿Cómo funciona el mapeo? Para entenderlo mejor, imaginemos primero una aplicación sin módulos. Queremos que las clases del presentador pertenezcan al espacio de nombres `App\Presentation`, de modo que el presentador `Home` se asigne a la clase `App\Presentation\HomePresenter`. Esto se consigue con esta configuración: + +```neon +application: + mapping: App\Presentation\*Presenter +``` + +El mapeo funciona sustituyendo el asterisco de la máscara `App\Presentation\*Presenter` por el nombre del presentador `Home`, dando como resultado final el nombre de la clase `App\Presentation\HomePresenter`. Muy sencillo. + +Sin embargo, como puede ver en los ejemplos de este y otros capítulos, colocamos las clases presentadoras en subdirectorios epónimos, por ejemplo, el presentador `Home` se asigna a la clase `App\Presentation\Home\HomePresenter`. Esto se consigue duplicando los dos puntos (requiere Nette Application 3.2): + +```neon +application: + mapping: App\Presentation\**Presenter +``` + +Ahora pasaremos a mapear presentadores en módulos. Podemos definir una asignación específica para cada módulo: + +```neon +application: + mapping: + Front: App\Presentation\Front\**Presenter + Admin: App\Presentation\Admin\**Presenter + Api: App\Api\*Presenter +``` + +Según esta configuración, el presentador `Front:Home` se asigna a la clase `App\Presentation\Front\Home\HomePresenter`, mientras que el presentador `Api:OAuth` se asigna a la clase `App\Api\OAuthPresenter`. + +Dado que los módulos `Front` y `Admin` tienen un método de asignación similar y que probablemente habrá más módulos de este tipo, es posible crear una regla general que los sustituya. Se añadirá un nuevo asterisco para el módulo a la máscara de clase: + +```neon +application: + mapping: + *: App\Presentation\*\**Presenter + Api: App\Api\*Presenter +``` + +También funciona para estructuras de directorios anidadas más profundas, como el presentador `Admin:User:Edit`, donde el segmento con asterisco se repite para cada nivel y da como resultado la clase `App\Presentation\Admin\User\Edit\EditPresenter`. + +Una notación alternativa consiste en utilizar una matriz formada por tres segmentos en lugar de una cadena. Esta notación es equivalente a la anterior: + +```neon +application: + mapping: + *: [App\Presentation, *, **Presenter] + Api: [App\Api, '', *Presenter] +``` diff --git a/application/es/how-it-works.texy b/application/es/how-it-works.texy index c23895a22f..8d3a0de8e0 100644 --- a/application/es/how-it-works.texy +++ b/application/es/how-it-works.texy @@ -22,18 +22,18 @@ La estructura de directorios se parece a esto /--pre web-project/ ├── app/ ← directorio con la aplicación -│ ├── Presenters/ ← clases para presentadores -│ │ ├── HomePresenter.php ← Home de inicio de la clase de presentador -│ │ └── templates/ ← directorio de plantillas -│ │ ├── @layout.latte ← plantilla de diseño compartida -│ │ └── Home/ ← plantillas para Home presentador de inicio -│ │ └── default.latte ← plantilla para la acción `default` -│ ├── Router/ ← configuración de direcciones URL +│ ├── Core/ ← clases básicas necesarias. +│ │ └── RouterFactory.php ← configuración de direcciones URL. +│ ├── Presentation/ ← presentadores, plantillas & co. +│ │ ├── @layout.latte ← plantilla de maquetación compartida +│ │ └── Home/ ← Home directorio del presentador +│ │ ├── HomePresenter.php ← Clase del presentador de inicio +│ │ └── default.latte ← plantilla para la acción default │ └── Bootstrap.php ← clase de arranque Bootstrap ├── bin/ ← scripts para la línea de comandos ├── config/ ← archivos de configuración │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← registros de errores ├── temp/ ← archivos temporales, caché, ... ├── vendor/ ← libraries installed by Composer @@ -45,9 +45,9 @@ La estructura de directorios se parece a esto └── .htaccess ← prohíbe el acceso a todos los directorios excepto www \-- -Puedes cambiar la estructura de directorios de cualquier forma, renombrar o mover carpetas, y entonces sólo tienes que editar las rutas a `log/` y `temp/` en el archivo `Bootstrap.php` y la ruta a este archivo en `composer.json` en la sección `autoload`. Nada más, sin reconfiguraciones complicadas ni cambios constantes. Nette tiene una [autodetección inteligente |bootstrap#development-vs-production-mode]. +Puedes modificar la estructura de directorios como quieras, renombrar o mover carpetas: es completamente flexible. Nette también cuenta con autodetección inteligente y reconoce automáticamente la ubicación de la aplicación, incluida su base URL. -Para aplicaciones un poco más grandes, podemos dividir las carpetas con presentadores y plantillas en subdirectorios (en el disco) y en espacios de nombres (en el código), que llamamos [módulos |modules]. +Para aplicaciones un poco más grandes, podemos organizar las carpetas de presentadores y plantillas en [subdirectorios |directory-structure#Presenters and templates] y agrupar las clases en espacios de nombres, a los que llamamos módulos. El directorio `www/` es el directorio público o raíz documental del proyecto. Puedes renombrarlo sin tener que configurar nada más en el lado de la aplicación. Solo necesitas [configurar el hosting |nette:troubleshooting#How to change or remove www directory from URL] para que el document-root vaya a este directorio. @@ -75,7 +75,7 @@ Su tarea es: ¿Qué tipo de fábrica? No fabricamos tractores, ¡sino páginas web! Espere, se lo explicaremos enseguida. -Por "inicializar el entorno" entendemos, por ejemplo, que se activa [Tracy |tracy:], que es una herramienta increíble para registrar o visualizar errores. Registra los errores en el servidor de producción y los muestra directamente en el servidor de desarrollo. Por lo tanto, la inicialización también necesita decidir si el sitio se está ejecutando en modo de producción o de desarrollo. Para ello, Nette utiliza la autodetección: si ejecuta el sitio en localhost, se ejecuta en modo desarrollador. No hay que configurar nada y la aplicación está lista tanto para el desarrollo como para el despliegue en producción. Estos pasos se realizan y describen en detalle en el capítulo sobre la [clase Bootstrap |bootstrap]. +Por "inicialización del entorno" entendemos, por ejemplo, activar [Tracy |tracy:], que es una fantástica herramienta de registro y visualización de errores. En los servidores de producción registra los errores, mientras que en los de desarrollo los muestra directamente. Por lo tanto, la inicialización incluye determinar si el sitio web se ejecuta en modo de producción o de desarrollo. Para ello, Nette utiliza [una autodetección inteligente |bootstrap#development-vs-production-mode]: si ejecuta el sitio en localhost, funciona en modo de desarrollo. No es necesaria ninguna configuración y la aplicación está lista tanto para el desarrollo como para el despliegue en producción. Estos pasos se realizan y detallan en el capítulo [de la clase Bootstrap |bootstrap]. El tercer punto (sí, nos hemos saltado el segundo, pero volveremos a él) es iniciar la aplicación. El manejo de las peticiones HTTP en Nette lo realiza la clase `Nette\Application\Application` (en adelante `Application`), por lo que cuando decimos "ejecutar una aplicación", nos referimos a llamar a un método con el nombre `run()` sobre un objeto de esta clase. @@ -91,7 +91,7 @@ Las aplicaciones escritas en Nette se dividen en muchos de los llamados presenta La aplicación comienza pidiendo al llamado enrutador que decida a cuál de los presentadores debe pasar la petición actual para su procesamiento. El enrutador decide de quién es la responsabilidad. Mira la URL de entrada `https://example.com/product/123`, que quiere `show` un producto con `id: 123` como acción. Es una buena costumbre escribir pares de presentador + acción separados por dos puntos como `Product:show`. -Así que el enrutador transforma la URL en un par `Presenter:action` + parámetros, en nuestro caso `Product:show` + `id: 123`. Puedes ver el aspecto de un enrutador en el archivo `app/Router/RouterFactory.php` y lo describiremos en detalle en el capítulo [Enrutamiento |Routing]. +Así que el enrutador transforma la URL en un par `Presenter:action` + parámetros, en nuestro caso `Product:show` + `id: 123`. Puedes ver el aspecto de un enrutador en el archivo `app/Core/RouterFactory.php` y lo describiremos en detalle en el capítulo [Enrutamiento |Routing]. Sigamos. La aplicación ya conoce el nombre del presentador y puede continuar. Creando un objeto `ProductPresenter`, que es el código del presentador `Product`. Más concretamente, le pide al contenedor DI que cree el presentador, porque producir objetos es su trabajo. @@ -121,12 +121,9 @@ Así, se llamó al método `renderShow(123)`, cuyo código es ficticio ejemplo, Posteriormente, el presentador devuelve la respuesta. Esta puede ser una página HTML, una imagen, un documento XML, el envío de un fichero desde disco, JSON o la redirección a otra página. Es importante destacar que, si no decimos explícitamente cómo responder (que es el caso de `ProductPresenter`), la respuesta será renderizar la plantilla con una página HTML. ¿Por qué? Pues porque en el 99% de los casos queremos dibujar una plantilla, así que el presentador toma este comportamiento por defecto y quiere facilitarnos el trabajo. Ese es el punto de Nette. -Ni siquiera tenemos que indicar qué plantilla dibujar, él deriva la ruta hacia ella según una lógica simple. En el caso del presentador `Product` y la acción `show`, intenta ver si uno de estos archivos de plantilla existe en relación al directorio donde se encuentra la clase `ProductPresenter`: +Ni siquiera necesitamos especificar qué plantilla renderizar; el framework deducirá la ruta por sí mismo. En el caso de la acción `show`, simplemente intenta cargar la plantilla `show.latte` en el directorio con la clase `ProductPresenter`. También intenta encontrar el diseño en el archivo `@layout.latte` (más información sobre la [búsqueda de plantillas |templates#Template Lookup]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -También intentará encontrar el diseño en el archivo `@layout.latte` y luego renderizará la plantilla. Ahora se completa la tarea del presentador y de toda la aplicación. Si la plantilla no existe, se devolverá una página con el error 404. Puedes leer más sobre los presentadores en la página de [Presentadores |Presenters]. +Posteriormente, se renderizan las plantillas. Esto completa la tarea del presentador y de toda la aplicación, y el trabajo está hecho. Si la plantilla no existiera, se devolvería una página de error 404. Puede leer más sobre los presentadores en la página [Presentadores |presenters]. [* request-flow.svg *] @@ -137,7 +134,7 @@ Sólo para estar seguros, intentemos recapitular todo el proceso con una URL lig 3) el router decodifica la URL como un par `Home:default` 4) se crea un objeto `HomePresenter` 5) se llama al método `renderDefault()` (si existe) -6) se renderiza una plantilla `templates/Home/default.latte` con un diseño `templates/@layout.latte` +6) se renderiza una plantilla `default.latte` con un diseño `@layout.latte` Puede que ahora te hayas encontrado con un montón de conceptos nuevos, pero creemos que tienen sentido. Crear aplicaciones en Nette es pan comido. diff --git a/application/es/modules.texy b/application/es/modules.texy deleted file mode 100644 index e183346e5f..0000000000 --- a/application/es/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Módulos -******* - -.[perex] -En Nette, los módulos representan las unidades lógicas que componen una aplicación. Incluyen presentadores, plantillas, posiblemente también componentes y clases modelo. - -Un directorio para los presentadores y otro para las plantillas no serían suficientes para los proyectos reales. Tener docenas de archivos en una carpeta es, como mínimo, desorganizado. ¿Cómo salir de ello? Simplemente los dividimos en subdirectorios en el disco y en espacios de nombres en el código. Y eso es exactamente lo que hacen los módulos de Nette. - -Así que olvidémonos de una única carpeta para presentadores y plantillas y en su lugar creemos módulos, por ejemplo `Admin` y `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← directorio con módulos -│ ├── Admin/ ← módulo Admin -│ │ ├── Presenters/ ← sus presentadores -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← módulo Front -│ └── Presenters/ ← sus presentadores -│ └── ... -\-- - -Esta estructura de directorios se reflejará en los espacios de nombres de las clases, así por ejemplo `DashboardPresenter` estará en el espacio de nombres `App\Modules\Admin\Presenters`: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -El presentador `Dashboard` dentro del módulo `Admin` es referenciado dentro de la aplicación usando la notación de dos puntos como `Admin:Dashboard`, y su acción `default` como `Admin:Dashboard:default`. -¿Y cómo sabe Nette que `Admin:Dashboard` representa la clase `App\Modules\Admin\Presenters\DashboardPresenter`? Esto se determina mediante el [mapeo |#mapping] en la configuración. -Por lo tanto, la estructura dada no es rígida y puede modificarla según sus necesidades. - -Por supuesto, los módulos pueden contener todos los demás elementos además de presentadores y plantillas, como componentes, clases modelo, etc. - - -Módulos anidados .[#toc-nested-modules] ---------------------------------------- - -Los módulos no tienen por qué formar sólo una estructura plana, también puedes crear submódulos, por ejemplo: - -/--pre -app/ -├── Modules/ ← directorio con módulos -│ ├── Blog/ ← módulo Blog -│ │ ├── Admin/ ← submódulo Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← submódulo Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← módulo Forum -│ │ └── ... -\-- - -Así, el módulo `Blog` se divide en los submódulos `Admin` y `Front`. De nuevo, esto se reflejará en los espacios de nombres, que serán `App\Modules\Blog\Admin\Presenters`, etc. El presentador `Dashboard` dentro del submódulo se denomina `Blog:Admin:Dashboard`. - -El anidamiento puede ser tan profundo como se desee, por lo que pueden crearse submódulos. - - -Creación de enlaces .[#toc-creating-links] ------------------------------------------- - -Los enlaces de las plantillas de presentador son relativos al módulo actual. Así, el enlace `Foo:default` lleva al presentador `Foo` en el mismo módulo que el presentador actual. Si el módulo actual es `Front`, por ejemplo, el enlace será el siguiente: - -```latte -enlace a Front:Product:show -``` - -Un enlace es relativo aunque incluya el nombre de un módulo, que se considera entonces un submódulo: - -```latte -enlace a Front:Shop:Product:show -``` - -Los enlaces absolutos se escriben de forma análoga a las rutas absolutas en disco, pero con dos puntos en lugar de barras. Así, un enlace absoluto comienza con dos puntos: - -```latte -enlace a Admin:Product:show -``` - -Para saber si estamos en un módulo determinado o en su submódulo podemos utilizar la función `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Enrutamiento .[#toc-routing] ----------------------------- - -Véase el [capítulo sobre en rutamiento|routing#Modules]. - - -Mapeo .[#toc-mapping] ---------------------- - -Define las reglas por las que el nombre de la clase se deriva del nombre del presentador. Las escribimos en [configuración |configuration] bajo la clave `application › mapping`. - -Empecemos con un ejemplo que no utiliza módulos. Sólo querremos que las clases del presentador tengan el espacio de nombres `App\Presenters`. Eso significa que un presentador como `Home` debe mapearse a la clase `App\Presenters\HomePresenter`. Esto se puede lograr con la siguiente configuración: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -El nombre del presentador se sustituye por el asterisco en la máscara de clase y el resultado es el nombre de la clase. Muy fácil. - -Si dividimos a los presentadores en módulos, podemos tener nuestra propia asignación para cada módulo: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Ahora el presentador `Front:Home` se asigna a la clase `App\Modules\Front\Presenters\HomePresenter` y el presentador `Admin:Dashboard` a la clase `App\Modules\Admin\Presenters\DashboardPresenter`. - -Es más práctico crear una regla general (estrella) para sustituir a las dos primeras. El asterisco adicional se añadirá a la máscara de clase sólo para el módulo: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Pero, ¿y si utilizamos módulos anidados y tenemos un presentador `Admin:User:Edit`? En este caso, el segmento con un asterisco que representa el módulo para cada nivel simplemente se repite y el resultado es la clase `App\Modules\Admin\User\Presenters\EditPresenter`. - -Una notación alternativa es utilizar una matriz formada por tres segmentos en lugar de una cadena. Esta notación es equivalente a la anterior: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -El valor por defecto es `*: *Module\*Presenter`. diff --git a/application/es/presenters.texy b/application/es/presenters.texy index c85c74305c..ee6f551d6c 100644 --- a/application/es/presenters.texy +++ b/application/es/presenters.texy @@ -60,7 +60,7 @@ Similar al método `render()`. Mientras que `render()` está destina Es importante que `action()` se llame antes que `render()`para que dentro de él podamos posiblemente cambiar el siguiente curso del ciclo de vida, es decir, cambiar la plantilla que será renderizada y también el método `render()` que será llamado, usando `setView('otherView')`. -Los parámetros de la petición se pasan al método. Es posible y recomendable especificar tipos para los parámetros, por ejemplo `actionShow(int $id, string $slug = null)` - si el parámetro `id` falta o si no es un entero, el presentador devuelve [el error 404 |#Error 404 etc.] y termina la operación. +Los parámetros de la petición se pasan al método. Es posible y recomendable especificar tipos para los parámetros, por ejemplo `actionShow(int $id, ?string $slug = null)` - si el parámetro `id` falta o si no es un entero, el presentador devuelve [el error 404 |#Error 404 etc.] y termina la operación. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ En la plantilla, estos mensajes están disponibles en la variable `$flashes` com Error 404 etc. .[#toc-error-404-etc] ==================================== -Cuando no podamos satisfacer la petición porque por ejemplo el artículo que queremos mostrar no existe en la base de datos, lanzaremos el error 404 utilizando el método `error(string $message = null, int $httpCode = 404)`, que representa el error HTTP 404: +Cuando no podamos satisfacer la petición porque por ejemplo el artículo que queremos mostrar no existe en la base de datos, lanzaremos el error 404 utilizando el método `error(?string $message = null, int $httpCode = 404)`, que representa el error HTTP 404: ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Parámetros de solicitud .[#toc-request-parameters] +================================================== + +El presentador, al igual que todos los componentes, obtiene sus parámetros de la petición HTTP. Sus valores pueden recuperarse utilizando el método `getParameter($name)` o `getParameters()`. Los valores son cadenas o matrices de cadenas, esencialmente datos sin procesar obtenidos directamente de la URL. + +Para mayor comodidad, recomendamos que los parámetros sean accesibles a través de propiedades. Basta con anotarlas con el atributo `#[Parameter]` atributo: + +```php +use Nette\Application\Attributes\Parameter; // esta línea es importante + +class HomePresenter extends Nette\Application\UI\Presenter +{ + #[Parameter] + public string $theme; // debe ser pública +} +``` + +Para las propiedades, le sugerimos que especifique el tipo de datos (por ejemplo, `string`). A continuación, Nette asignará automáticamente el valor basándose en él. Los valores de los parámetros también pueden [validarse |#Validation of Parameters]. + +Al crear un enlace, puede establecer directamente el valor de los parámetros: + +```latte +click +``` + + Parámetros persistentes .[#toc-persistent-parameters] ===================================================== @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Si `$this->lang` tiene un valor como `'en'`, entonces los enlaces creados usando `link()` o `n:href` también contendrán el parámetro `lang=en`. Y cuando se haga clic en el enlace, volverá a ser `$this->lang = 'en'`. -Para las propiedades, se recomienda incluir el tipo de datos (por ejemplo, `string`) y también se puede incluir un valor por defecto. Los valores de los parámetros se pueden [validar |#Validation of Persistent Parameters]. +Para las propiedades, le recomendamos que incluya el tipo de datos (por ejemplo, `string`) y también puede incluir un valor por defecto. Los valores de los parámetros se pueden [validar |#Validation of Parameters]. Los parámetros persistentes se pasan entre todas las acciones de un presentador determinado por defecto. Para pasarlos entre varios presentadores, es necesario definirlos: @@ -307,18 +333,12 @@ Profundizando .[#toc-going-deeper] Lo que hemos mostrado hasta ahora en este capítulo probablemente será suficiente. Las líneas siguientes están pensadas para quienes estén interesados en los presentadores en profundidad y quieran saberlo todo. -Requisitos y parámetros .[#toc-requirement-and-parameters] ----------------------------------------------------------- +Validación de parámetros .[#toc-validation-of-parameters] +--------------------------------------------------------- -La solicitud gestionada por el presentador es el objeto [api:Nette\Application\Request] y es devuelta por el método del presentador `getRequest()`. Incluye una matriz de parámetros y cada uno de ellos pertenece a alguno de los componentes o directamente al presentador (que en realidad también es un componente, aunque especial). Así pues, Nette redistribuye los parámetros y los pasa entre los distintos componentes (y el presentador) llamando al método `loadState(array $params)`. Los parámetros pueden obtenerse mediante el método `getParameters(): array`, individualmente mediante `getParameter($name)`. Los valores de los parámetros son cadenas o matrices de cadenas, son básicamente datos en bruto obtenidos directamente de una URL. +Los valores de los parámetros de [petición |#request parameters] y [de los parámetros persistentes |#persistent parameters] recibidos de las URLs son escritos en propiedades por el método `loadState()`. También comprueba si el tipo de datos especificado en la propiedad coincide, de lo contrario responderá con un error 404 y la página no se mostrará. - -Validación de parámetros persistentes .[#toc-validation-of-persistent-parameters] ---------------------------------------------------------------------------------- - -Los valores de los parámetros [persistentes |#persistent parameters] recibidos de las URLs son escritos en propiedades por el método `loadState()`. También comprueba si el tipo de datos especificado en la propiedad coincide, de lo contrario responderá con un error 404 y no se mostrará la página. - -Nunca confíes ciegamente en los parámetros persistentes, ya que pueden ser fácilmente sobrescritos por el usuario en la URL. Por ejemplo, así es como comprobamos si `$this->lang` está entre los idiomas soportados. Una buena forma de hacerlo es sobrescribir el método `loadState()` mencionado anteriormente: +Nunca confíes ciegamente en los parámetros, ya que pueden ser fácilmente sobrescritos por el usuario en la URL. Por ejemplo, así es como comprobamos si `$this->lang` está entre los idiomas soportados. Una buena forma de hacerlo es sobrescribir el método `loadState()` mencionado anteriormente: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,7 +361,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Guardar y restaurar la petición .[#toc-save-and-restore-the-request] -------------------------------------------------------------------- -Puede guardar la solicitud actual en una sesión o restaurarla desde la sesión y dejar que el presentador la ejecute de nuevo. Esto es útil, por ejemplo, cuando un usuario rellena un formulario y su login expira. Para no perder datos, antes de redirigir a la página de inicio de sesión, guardamos la solicitud actual en la sesión utilizando `$reqId = $this->storeRequest()`, que devuelve un identificador en forma de cadena corta y lo pasa como parámetro al presentador de inicio de sesión. +La solicitud que gestiona el presentador es un objeto [api:Nette\Application\Request] y la devuelve el método del presentador `getRequest()`. + +Puede guardar la solicitud actual en una sesión o restaurarla desde la sesión y dejar que el presentador la ejecute de nuevo. Esto es útil, por ejemplo, cuando un usuario rellena un formulario y su login caduca. Para no perder datos, antes de redirigir a la página de inicio de sesión, guardamos la solicitud actual en la sesión mediante `$reqId = $this->storeRequest()`, que devuelve un identificador en forma de cadena corta y lo pasa como parámetro al presentador de inicio de sesión. Después de iniciar sesión, llamamos al método `$this->restoreRequest($reqId)`, que recoge la solicitud de la sesión y se la reenvía. El método verifica que la petición ha sido creada por el mismo usuario que ahora ha iniciado la sesión. Si otro usuario inicia sesión o la clave no es válida, no hace nada y el programa continúa. @@ -362,7 +384,7 @@ La redirección no se produce con una solicitud AJAX o POST porque provocaría u También puede invocar la canonización manualmente mediante el método `canonicalize()`, que, al igual que el método `link()`, recibe el presentador, las acciones y los parámetros como argumentos. Crea un enlace y lo compara con la URL actual. Si es diferente, redirige al enlace generado. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // redirects if $slug is different from $realSlug @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Restricción de acceso mediante `#[Requires]` .[#toc-access-restriction-using-requires]{data-version:3.2.2} +---------------------------------------------------------------------------------------------------------- + +El atributo `#[Requires]` ofrece opciones avanzadas para restringir el acceso a los presentadores y sus métodos. Puede utilizarse para especificar métodos HTTP, requerir solicitudes AJAX, limitar el acceso al mismo origen y restringir el acceso sólo al reenvío. El atributo puede aplicarse a clases de presentadores, así como a métodos individuales como `action()`, `render()`, `handle()`y `createComponent()`. + +Puede especificar estas restricciones +- en los métodos HTTP: `#[Requires(methods: ['GET', 'POST'])]` +- que requieren una petición AJAX: `#[Requires(ajax: true)]` +- acceso sólo desde el mismo origen: `#[Requires(sameOrigin: true)]` +- acceso sólo mediante reenvío: `#[Requires(forward: true)]` +- restricciones sobre acciones específicas: `#[Requires(actions: 'default')]` + +Para obtener más información, consulte [Cómo utilizar el atributo Requires atributo |best-practices:attribute-requires]. + + +Comprobación del método HTTP .[#toc-http-method-check] +------------------------------------------------------ + +En Nette, los presentadores verifican automáticamente el método HTTP de cada solicitud entrante, principalmente por razones de seguridad. Por defecto, se permiten los métodos `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. + +Si desea habilitar métodos adicionales como `OPTIONS`, puede utilizar el atributo `#[Requires]` (a partir de Nette Application v3.2): + +```php +#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] +class MyPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +En la versión 3.1, la verificación se realiza en `checkHttpMethod()`, que comprueba si el método especificado en la petición está incluido en el array `$presenter->allowedMethods`. Añade un método como este + +```php +class MyPresenter extends Nette\Application\UI\Presenter +{ + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } +} +``` + +Es crucial enfatizar que si permite el método `OPTIONS`, también debe manejarlo adecuadamente dentro de su presentador. Este método se utiliza a menudo como una solicitud de comprobación previa, que los navegadores envían automáticamente antes de la solicitud real cuando es necesario determinar si la solicitud está permitida desde el punto de vista de la política CORS (Cross-Origin Resource Sharing). Si permite este método pero no implementa una respuesta adecuada, puede provocar incoherencias y posibles problemas de seguridad. + + Lecturas complementarias .[#toc-further-reading] ================================================ diff --git a/application/es/routing.texy b/application/es/routing.texy index c6ed0f3950..f574ce9016 100644 --- a/application/es/routing.texy +++ b/application/es/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -O podemos utilizar esta forma, observe la reescritura de la expresión regular de validación: +Para una especificación más detallada, se puede utilizar una forma aún más extendida, en la que además de los valores por defecto, se pueden establecer otras propiedades de los parámetros, como una expresión regular de validación (véase el parámetro `id` ): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Estos formatos más locuaces son útiles para añadir otros metadatos. +Es importante señalar que si los parámetros definidos en la matriz no se incluyen en la máscara de ruta, sus valores no podrán modificarse, ni siquiera utilizando parámetros de consulta especificados tras un signo de interrogación en la URL. Filtros y traducciones .[#toc-filters-and-translations] @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Módulos .[#toc-modules] ----------------------- -Si tenemos más rutas que pertenecen a un [módulo |modules], podemos utilizar `withModule()` para agruparlas: +Si tenemos más rutas que pertenecen a un [módulo |directory-structure#Presenters and Templates], podemos utilizar `withModule()` para agruparlas: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Integración .[#toc-integration] =============================== -Para conectar nuestro router a la aplicación, debemos informar al contenedor DI sobre él. La forma más sencilla es preparar la fábrica que construirá el objeto router y decirle a la configuración del contenedor que lo utilice. Digamos que escribimos un método para este propósito `App\Router\RouterFactory::createRouter()`: +Para conectar nuestro router a la aplicación, debemos informar al contenedor DI sobre él. La forma más sencilla es preparar la fábrica que construirá el objeto router y decirle a la configuración del contenedor que lo utilice. Digamos que escribimos un método para este propósito `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ Luego escribimos en [configuración |dependency-injection:services]: ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Cualquier dependencia, como una conexión de base de datos, etc., se pasa al método de fábrica como sus parámetros utilizando [autowiring |dependency-injection:autowiring]: @@ -663,7 +663,7 @@ Por uso separado, nos referimos al uso de las capacidades del router en una apli Así que de nuevo crearemos un método que construirá un enrutador, por ejemplo ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); O crearemos los objetos directamente: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/es/templates.texy b/application/es/templates.texy index 761f7a648e..f4391ddaa3 100644 --- a/application/es/templates.texy +++ b/application/es/templates.texy @@ -34,35 +34,81 @@ Y esta podría ser la plantilla de acción: Define el bloque `content`, que se inserta en lugar de `{include content}` en el diseño, y también redefine el bloque `title`, que sobrescribe `{block title}` en el diseño. Intenta imaginar el resultado. -Búsqueda de plantillas .[#toc-search-for-templates] ---------------------------------------------------- +Búsqueda de plantillas .[#toc-template-lookup] +---------------------------------------------- -La ruta a las plantillas se deduce según una lógica simple. Se intenta ver si uno de estos archivos de plantilla existe en relación con el directorio donde se encuentra la clase de presentador, donde `` es el nombre del presentador actual y `` es el nombre de la acción actual: +En los presentadores, no es necesario especificar qué plantilla debe renderizarse; el framework determinará automáticamente la ruta, facilitándole la codificación. -- `templates//.latte` -- `templates/..latte` +Si utiliza una estructura de directorios donde cada presentador tiene su propio directorio, simplemente coloque la plantilla en este directorio bajo el nombre de la acción (es decir, vista). Por ejemplo, para la acción `default`, utilice la plantilla `default.latte`: -Si no se encuentra la plantilla, se intentará buscar en el directorio `templates` un nivel más arriba, es decir, al mismo nivel que el directorio con la clase presentadora. +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -Si la plantilla tampoco se encuentra allí, la respuesta es un [error 404 |presenters#Error 404 etc.]. +Si utiliza una estructura en la que los presentadores están juntos en un directorio y las plantillas en una carpeta `templates`, guárdela en un archivo `..latte` o en `/.latte`: -También puede cambiar la vista utilizando `$this->setView('otherView')`. O, en lugar de buscar, especifique directamente el nombre del archivo de plantilla utilizando `$this->template->setFile('/path/to/template.latte')`. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- + +El directorio `templates` también puede colocarse un nivel más arriba, al mismo nivel que el directorio con las clases de presentador. + +Si no se encuentra la plantilla, el presentador responde con el [error 404 - página no encontrada |presenters#Error 404 etc]. + +Puede cambiar la vista utilizando `$this->setView('anotherView')`. También es posible especificar directamente el archivo de plantilla con `$this->template->setFile('/path/to/template.latte')`. .[note] -Puede cambiar las rutas donde se buscan las plantillas anulando el método [formatTemplateFiles |api:Nette\Application\UI\Presenter::formatTemplateFiles()], que devuelve una matriz de posibles rutas de archivo. +Los archivos en los que se buscan las plantillas pueden cambiarse anulando el método [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], que devuelve una matriz de posibles nombres de archivo. + + +Búsqueda de plantillas de diseño .[#toc-layout-template-lookup] +--------------------------------------------------------------- + +Nette también busca automáticamente el archivo de diseño. + +Si utiliza una estructura de directorios en la que cada presentador tiene su propio directorio, coloque la maqueta en la carpeta con el presentador, si es específica sólo para él, o en un nivel superior si es común a varios presentadores: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +Si utiliza una estructura en la que los presentadores están agrupados en un directorio y las plantillas se encuentran en una carpeta `templates`, la maquetación se esperará en los siguientes lugares: -El diseño se espera en los siguientes archivos: +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` diseño común a varios presentadores +Si el presentador se encuentra en un módulo, también buscará más arriba en el árbol de directorios según el anidamiento del módulo. -`` es el nombre del presentador actual y `` es el nombre de la maquetación, que por defecto es `'layout'`. El nombre puede cambiarse con `$this->setLayout('otherLayout')`, de modo que se intentarán los archivos `@otherLayout.latte`. +El nombre de la presentación puede cambiarse utilizando `$this->setLayout('layoutAdmin')` y entonces se esperará en el archivo `@layoutAdmin.latte`. También puede especificar directamente el archivo de plantilla de presentación utilizando `$this->setLayout('/path/to/template.latte')`. -También puede especificar directamente el nombre de archivo de la plantilla de maquetación con `$this->setLayout('/path/to/template.latte')`. El uso de `$this->setLayout(false)` desactivará la búsqueda de diseños. +El uso de `$this->setLayout(false)` o de la etiqueta `{layout none}` dentro de la plantilla desactiva la búsqueda de diseños. .[note] -Puede cambiar las rutas donde se buscan las plantillas anulando el método [formatLayoutTemplateFiles |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], que devuelve una matriz de posibles rutas de archivo. +Los archivos en los que se buscan las plantillas de diseño pueden modificarse modificando el método [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], que devuelve una matriz de posibles nombres de archivo. Variables en la plantilla .[#toc-variables-in-the-template] @@ -104,7 +150,7 @@ La anotación `@property-read` es para IDE y análisis estático, hará que func Puedes permitirte el lujo de susurrar en las plantillas también, simplemente instala el plugin Latte en PhpStorm y especifica el nombre de la clase al principio de la plantilla, ver el artículo "Latte: cómo escribir sistema:https://blog.nette.org/es/latte-como-utilizar-el-sistema-de-tipos": ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void La versión 3 de Latte ofrece una forma más avanzada creando una [extensión |latte:creating-extension] para cada proyecto web. He aquí un ejemplo aproximado de una clase de este tipo: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ La registramos usando [configuration|configuration#Latte]: ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ Alternativamente, el traductor se puede establecer utilizando la [configuración ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` El traductor puede utilizarse, por ejemplo, como un filtro `|translate`, con parámetros adicionales pasados al método `translate()` (véase `foo, bar`): diff --git a/application/fr/@home.texy b/application/fr/@home.texy index f0388b9636..4b11007fbb 100644 --- a/application/fr/@home.texy +++ b/application/fr/@home.texy @@ -2,35 +2,84 @@ Application Nette ***************** .[perex] -Le paquet `nette/application` est la base de la création d'applications web interactives. +Nette Application est le cœur du framework Nette qui apporte des outils puissants pour créer des applications web modernes. Il offre de nombreuses fonctionnalités exceptionnelles qui simplifient considérablement le développement et améliorent la sécurité et la maintenabilité du code. -- [Comment fonctionnent les applications ? |how-it-works] -- [Bootstrap] -- [Présentateurs |Presenters] -- [Modèles |Templates] -- [Modules] -- [Acheminement |Routing] -- [Création de liens URL |creating-links] -- [Composants interactifs |components] -- [AJAX et Snippets |ajax] -- [Multiplicateur |multiplier] -- [Configuration] +Installation .[#toc-installation] +--------------------------------- -Installation ------------- - -Téléchargez et installez le paquet en utilisant [Composer |best-practices:composer]: +Téléchargez et installez la bibliothèque à l'aide de [Composer |best-practices:composer]: ```shell composer require nette/application ``` -| version | compatible avec PHP + +Pourquoi choisir Nette Application ? .[#toc-why-choose-nette-application] +------------------------------------------------------------------------- + +Nette a toujours été un pionnier en matière de technologies web. + +**Routeur bidirectionnel:** Nette dispose d'un système de routage avancé unique dans sa bidirectionnalité - il ne traduit pas seulement les URLs en actions d'application mais peut aussi générer des URLs en sens inverse. Cela signifie que : +- Vous pouvez modifier la structure des URL de l'ensemble de l'application à tout moment sans modifier les fichiers modèles. +- Les URL sont automatiquement canonisées, ce qui améliore le référencement. +- Le routage est défini en un seul endroit, et non dispersé dans des annotations. + +**Composants et signaux:** Le système de composants intégré inspiré de Delphi et React.js est unique parmi les frameworks PHP : +- Permet de créer des éléments d'interface utilisateur réutilisables +- Supporte la composition hiérarchique des composants +- Offre une gestion élégante des requêtes AJAX en utilisant des signaux +- Riche bibliothèque de composants prêts à l'emploi sur [Componette](https://componette.org) + +**AJAX et Snippets:** Nette a introduit une façon révolutionnaire de travailler avec AJAX en 2009, avant des solutions comme Hotwire pour Ruby on Rails ou Symfony UX Turbo : +- Les snippets permettent de ne mettre à jour que des parties de la page sans écrire de JavaScript +- Intégration automatique avec le système de composants +- Invalidation intelligente des sections de la page +- Transfert minimal de données + +**Modèles intuitifs [Latte |latte:]:** Le système de création de modèles le plus sûr pour PHP avec des fonctionnalités avancées : +- Protection XSS automatique avec échappement contextuel +- Extensible avec des filtres, des fonctions et des balises personnalisés +- Héritage de modèles et snippets pour AJAX +- Excellent support de PHP 8.x avec système de type + +**Injection de dépendances:** Nette utilise pleinement l'injection de dépendances : +- Passage automatique des dépendances (autowiring) +- Configuration utilisant le format NEON clair +- Prise en charge des usines de composants + + +Principaux avantages .[#toc-main-benefits] +------------------------------------------ + +- Sécurité** : Protection automatique contre les [vulnérabilités |nette:vulnerability-protection] telles que XSS, CSRF, etc. +- Productivité** : Moins d'écriture, plus de fonctionnalités grâce à une conception intelligente +- Débogage** : [Débogage Tracy |tracy:] avec panneau de routage +- Performance** : Système de cache intelligent, chargement paresseux des composants +- Flexibilité Modification facile de l'URL même après l'achèvement de l'application +- Composants** : Système unique d'éléments d'interface utilisateur réutilisables +- Moderne** : Prise en charge complète de PHP 8.4+ et du système de type + + +Pour commencer .[#toc-getting-started] +-------------------------------------- + +1. Comprendre [les applications |how-it-works] - Comprendre l'architecture de base +2. [Présentateurs |presenters] - Travailler avec des présentateurs et des actions +3. [Modèles |templates] - Créer des modèles dans Latte +4. [Routage |routing] - Configuration des URL +5. [Composants interactifs |components] - Utilisation du système de composants + + +Compatibilité PHP .[#toc-php-compatibility] +------------------------------------------- + +| Version compatible avec PHP |-----------|------------------- -| Nette Application 4.0 | PHP 8.0 - 8.2 -| Nette Application 3.1 | PHP 7.2 - 8.2 +| Nette Application 4.0 | PHP 8.1 - 8.4 +| Nette Application 3.2 | PHP 8.1 - 8.4 +| Nette Application 3.1 | PHP 7.2 - 8.3 | Nette Application 3.0 | PHP 7.1 - 8.0 | Nette Application 2.4 | PHP 5.6 - 8.0 -S'applique aux dernières versions du patch. +Valable pour les dernières versions des correctifs. \ No newline at end of file diff --git a/application/fr/@left-menu.texy b/application/fr/@left-menu.texy index 38078e6bf0..9c09ce3054 100644 --- a/application/fr/@left-menu.texy +++ b/application/fr/@left-menu.texy @@ -4,7 +4,7 @@ Application Nette - [Bootstrap] - [Présentateurs |Presenters] - [Modèles |Templates] -- [Modules] +- [Structure du répertoire |directory-structure] - [Acheminement |Routing] - [Création de liens URL |creating-links] - [Composants interactifs |components] diff --git a/application/fr/ajax.texy b/application/fr/ajax.texy index 55c07c6dc6..d93abac2f4 100644 --- a/application/fr/ajax.texy +++ b/application/fr/ajax.texy @@ -1,12 +1,12 @@ -AJAX et Snippets -**************** +AJAX & Snippets +***************
    -Les applications web modernes fonctionnent aujourd'hui pour moitié sur un serveur et pour moitié dans un navigateur. AJAX est un facteur d'unité essentiel. Quel est le support offert par le Nette Framework ? -- l'envoi de fragments de modèles (appelés *snippets*) +À l'ère des applications web modernes, où les fonctionnalités s'étendent souvent entre le serveur et le navigateur, AJAX est un élément de connexion essentiel. Quelles sont les possibilités offertes par le Nette Framework dans ce domaine ? +- l'envoi de parties du modèle, appelées "snippets - le passage de variables entre PHP et JavaScript -- débogage des applications AJAX +- outils de débogage des requêtes AJAX
    @@ -14,29 +14,32 @@ Les applications web modernes fonctionnent aujourd'hui pour moitié sur un serve Demande AJAX .[#toc-ajax-request] ================================= -Une requête AJAX ne diffère pas d'une requête classique : le diffuseur est appelé avec une vue et des paramètres spécifiques. C'est également au présentateur de décider comment y répondre : il peut utiliser sa propre routine, qui renvoie un fragment de code HTML (extrait HTML), un document XML, un objet JSON ou du code JavaScript. +Une requête AJAX ne diffère pas fondamentalement d'une requête HTTP classique. Un présentateur est appelé avec des paramètres spécifiques. C'est au présentateur de décider comment répondre à la requête - il peut renvoyer des données au format JSON, envoyer une partie du code HTML, un document XML, etc. -Côté serveur, une requête AJAX peut être détectée à l'aide de la méthode de service [encapsulant la requête HTTP |http:request] `$httpRequest->isAjax()` (détection basée sur l'en-tête HTTP `X-Requested-With`). Dans le présentateur, un raccourci est disponible sous la forme de la méthode `$this->isAjax()`. +Du côté du navigateur, nous lançons une requête AJAX à l'aide de la fonction `fetch()`: -Il existe un objet prétraité appelé `payload` dédié à l'envoi de données au navigateur en JSON. - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // traitement de la réponse +}); ``` -Pour un contrôle total de votre sortie JSON, utilisez la méthode `sendJson` dans votre présentateur. Elle met immédiatement fin au présentateur et vous vous passerez de modèle : +Du côté du serveur, une requête AJAX est reconnue par la méthode `$httpRequest->isAjax()` du service [encapsulant la requête HTTP |http:request]. Elle utilise l'en-tête HTTP `X-Requested-With`, qu'il est donc essentiel d'envoyer. Dans le présentateur, vous pouvez utiliser la méthode `$this->isAjax()`. + +Si vous souhaitez envoyer des données au format JSON, utilisez la méthode [`sendJson()` |presenters#Sending a response] méthode. La méthode met également fin à l'activité du présentateur. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Si nous voulons envoyer du HTML, nous pouvons soit définir un modèle spécial pour les demandes AJAX : +Si vous envisagez de répondre avec un modèle spécial conçu pour AJAX, vous pouvez procéder comme suit : ```php public function handleClick($param): void @@ -44,27 +47,43 @@ public function handleClick($param): void if ($this->isAjax()) { $this->template->setFile('path/to/ajax.latte'); } - // ... + //... } ``` +Bribes .[#toc-snippets] +======================= + +Les snippets sont l'outil le plus puissant offert par Nette pour connecter le serveur au client. Grâce à eux, vous pouvez transformer une application ordinaire en une application AJAX avec un minimum d'effort et quelques lignes de code. L'exemple Fifteen montre comment tout cela fonctionne, et son code peut être trouvé sur [GitHub |https://github.com/nette-examples/fifteen]. + +Les snippets, ou clippings, vous permettent de ne mettre à jour que certaines parties de la page, au lieu de recharger toute la page. C'est plus rapide et plus efficace, et l'expérience utilisateur est plus confortable. Les snippets peuvent vous rappeler Hotwire pour Ruby on Rails ou Symfony UX Turbo. Il est intéressant de noter que Nette a introduit les snippets 14 ans plus tôt. + +Comment fonctionnent les snippets ? Lorsque la page est chargée pour la première fois (requête non-AJAX), la page entière, y compris tous les snippets, est chargée. Lorsque l'utilisateur interagit avec la page (par exemple, lorsqu'il clique sur un bouton, soumet un formulaire, etc.), au lieu de charger la page entière, une requête AJAX est effectuée. Le code du présentateur exécute l'action et décide quels extraits doivent être mis à jour. Nette rend ces extraits et les envoie sous la forme d'un tableau JSON. Le code de traitement du navigateur réintègre alors les extraits reçus dans la page. Par conséquent, seul le code des extraits modifiés est transféré, ce qui permet d'économiser de la bande passante et d'accélérer le chargement par rapport au transfert de l'ensemble du contenu de la page. + + Naja .[#toc-naja] -================= +----------------- -La [bibliothèque Naja |https://naja.js.org] est utilisée pour gérer les requêtes AJAX du côté du navigateur. [Installez-la |https://naja.js.org/#/guide/01-install-setup-naja] en tant que paquet node.js (à utiliser avec Webpack, Rollup, Vite, Parcel et plus) : +La [bibliothèque Naja |https://naja.js.org] est utilisée pour gérer les snippets du côté du navigateur. [Installez-la |https://naja.js.org/#/guide/01-install-setup-naja] en tant que paquetage node.js (pour une utilisation avec des applications telles que Webpack, Rollup, Vite, Parcel, et d'autres) : ```shell npm install naja ``` -...ou insérez-la directement dans le modèle de page : +... ou insérez-la directement dans le modèle de page : ```html ``` -Pour créer une requête AJAX à partir d'un lien normal (signal) ou d'une soumission de formulaire, il suffit de marquer le lien, le formulaire ou le bouton concerné avec la classe `ajax`: +Vous devez d'abord [initialiser |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] la bibliothèque : + +```js +naja.initialize(); +``` + +Pour faire d'un lien ordinaire (signal) ou d'une soumission de formulaire une requête AJAX, il suffit de marquer le lien, le formulaire ou le bouton correspondant avec la classe `ajax`: ```html Go @@ -74,64 +93,39 @@ Pour créer une requête AJAX à partir d'un lien normal (signal) ou d'une soumi or +
    ``` -Extraits de texte .[#toc-snippets] -================================== - -Il existe un outil bien plus puissant que le support AJAX intégré : les snippets. Leur utilisation permet de transformer une application ordinaire en une application AJAX en utilisant seulement quelques lignes de code. La façon dont tout cela fonctionne est démontrée dans l'exemple Fifteen dont le code est également accessible dans le build ou sur [GitHub |https://github.com/nette-examples/fifteen]. - -Le fonctionnement des snippets est le suivant : la page entière est transférée lors de la requête initiale (c'est-à-dire non-AJAX), puis à chaque [sous-requête |components#signal] AJAX (requête de la même vue du même présentateur), seul le code des parties modifiées est transféré dans le dépôt `payload` mentionné précédemment. - -Les snippets vous rappellent peut-être Hotwire pour Ruby on Rails ou Symfony UX Turbo, mais Nette les a inventés quatorze ans plus tôt. - +Redessiner des extraits .[#toc-redrawing-snippets] +-------------------------------------------------- -Invalidation des Snippets .[#toc-invalidation-of-snippets] -========================================================== - -Chaque descendant de la classe [Control |components] (ce qu'est aussi un Presenter) est capable de se souvenir si des changements sont intervenus au cours d'une requête qui nécessitent un nouveau rendu. Il existe une paire de méthodes pour gérer cela : `redrawControl()` et `isControlInvalid()`. Un exemple : +Chaque objet de la classe [Control |components] (y compris le Presenter lui-même) garde une trace des changements survenus qui nécessitent son redécoupage. La méthode `redrawControl()` est utilisée à cette fin. ```php public function handleLogin(string $user): void { - // L'objet doit être rendu à nouveau après que l'utilisateur se soit connecté. + // après la connexion, il est nécessaire de redessiner la partie concernée $this->redrawControl(); - // ... + //... } ``` -Nette offre cependant une résolution encore plus fine que les composants entiers. Les méthodes listées acceptent le nom d'un "snippet" comme paramètre optionnel. Un "snippet" est en fait un élément de votre modèle marqué à cet effet par une tag Latte, nous y reviendrons plus tard. Il est donc possible de demander à un composant de ne redessiner que des *parties* de son modèle. Si le composant entier est invalidé, tous ses snippets sont redessinés. Un composant est également "invalide" si l'un de ses sous-composants est invalide. - -```php -$this->isControlInvalid(); // -> false -$this->redrawControl('header'); // invalide le snippet nommé 'header'. -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, au moins un extrait est invalide. +Nette permet également un contrôle plus fin de ce qui doit être redessiné. La méthode susmentionnée peut prendre le nom de l'extrait comme argument. Ainsi, il est possible d'invalider (c'est-à-dire de forcer un nouveau dessin) au niveau de la partie du modèle. Si l'ensemble du composant est invalidé, chaque extrait est également redessiné : -$this->redrawControl(); // invalide l'ensemble du composant, chaque extrait. -$this->isControlInvalid('footer'); // -> true +```php +// invalide l'extrait "header" (en-tête) +$this->redrawControl('header'); ``` -Un composant qui reçoit un signal est automatiquement marqué pour être redessiné. - -Grâce au redessin de snippet, nous savons exactement quelles parties de quels éléments doivent être redessinées. - - -Balise `{snippet} … {/snippet}` .{toc: Tag snippet} -=================================================== - -Le rendu de la page se déroule de manière très similaire à une requête ordinaire : les mêmes modèles sont chargés, etc. L'essentiel est toutefois de laisser de côté les parties qui ne sont pas censées atteindre la sortie ; les autres parties doivent être associées à un identifiant et envoyées à l'utilisateur dans un format compréhensible pour un gestionnaire JavaScript. - -Syntaxe .[#toc-syntax] ----------------------- +Bribes dans Latte .[#toc-snippets-in-latte] +------------------------------------------- -Si le modèle contient un contrôle ou un extrait, nous devons l'envelopper à l'aide de la balise de paire `{snippet} ... {/snippet}`. Elle veillera à ce que l'extrait rendu soit "découpé" et envoyé au navigateur. Elle l'enfermera également dans une balise auxiliaire `
    ` (il est possible d'en utiliser une autre). Dans l'exemple suivant, un extrait nommé `header` est défini. Il peut tout aussi bien représenter le modèle d'un composant : +L'utilisation des snippets dans Latte est extrêmement simple. Pour définir une partie du modèle comme un extrait, il suffit de l'entourer des balises `{snippet}` et `{/snippet}`: ```latte {snippet header} @@ -139,7 +133,9 @@ Si le modèle contient un contrôle ou un extrait, nous devons l'envelopper à l {/snippet} ``` -Si vous souhaitez créer un snippet avec un élément contenant différent de `
    ` ou ajouter des attributs personnalisés à l'élément, vous pouvez utiliser la définition suivante : +Le snippet crée un élément `
    ` dans la page HTML avec un `id` spécialement généré. Lorsqu'un extrait est redessiné, le contenu de cet élément est mis à jour. Par conséquent, lors du rendu initial de la page, tous les snippets doivent également être rendus, même s'ils sont initialement vides. + +Vous pouvez également créer un extrait avec un élément autre que `
    ` à l'aide d'un attribut n: : ```latte
    @@ -148,138 +144,106 @@ Si vous souhaitez créer un snippet avec un élément contenant différent de `< ``` -Dynamic Snippets .[#toc-dynamic-snippets] -========================================= +Zones d'échantillonnage .[#toc-snippet-areas] +--------------------------------------------- -Dans Nette, vous pouvez également définir des snippets avec un nom dynamique basé sur un paramètre d'exécution. C'est la solution la plus appropriée pour les listes diverses où nous devons modifier une seule ligne mais où nous ne voulons pas transférer toute la liste avec elle. Un exemple de ceci serait : +Les noms des snippets peuvent également être des expressions : ```latte -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Il y a un snippet statique appelé `itemsContainer`, contenant plusieurs snippets dynamiques : `item-0`, `item-1` et ainsi de suite. +De cette manière, nous obtiendrons plusieurs snippets comme `item-0`, `item-1`, etc. Si nous devions invalider directement un extrait dynamique (par exemple, `item-1`), rien ne serait redessiné. La raison en est que les snippets fonctionnent comme de véritables extraits et qu'ils sont les seuls à être rendus directement. Cependant, dans le modèle, il n'y a pas techniquement d'extrait nommé `item-1`. Il n'apparaît que lors de l'exécution du code environnant de l'extrait, dans ce cas, la boucle foreach. Par conséquent, nous marquerons la partie du modèle qui doit être exécutée avec la balise `{snippetArea}`: -Vous ne pouvez pas redessiner un extrait dynamique directement (redessiner `item-1` n'a aucun effet), vous devez redessiner son extrait parent (dans cet exemple `itemsContainer`). Le code du snippet parent est alors exécuté, mais seuls ses sous-snippets sont envoyés au navigateur. Si vous souhaitez n'envoyer qu'un seul des sous-ensembles, vous devez modifier l'entrée du snippet parent pour ne pas générer les autres sous-ensembles. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -Dans l'exemple ci-dessus, vous devez vous assurer que, pour une requête AJAX, un seul élément sera ajouté au tableau `$list`. Par conséquent, la boucle `foreach` n'imprimera qu'un seul extrait dynamique. +Et nous redessinerons à la fois l'extrait individuel et l'ensemble de la zone globale : ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * This method returns data for the list. - * Usually this would just request the data from a model. - * For the purpose of this example, the data is hard-coded. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +Il est également essentiel de s'assurer que le tableau `$items` ne contient que les éléments qui doivent être redessinés. -Extraits dans un modèle inclus .[#toc-snippets-in-an-included-template] -======================================================================= - -Il peut arriver que le snippet se trouve dans un modèle qui est inclus à partir d'un autre modèle. Dans ce cas, nous devons envelopper le code d'inclusion dans le second modèle avec la tag `snippetArea`, puis nous redessinons à la fois la snippetArea et le snippet lui-même. - -La tag `snippetArea` garantit que le code qu'elle contient est exécuté mais que seul l'extrait réel du modèle inclus est envoyé au navigateur. +Lors de l'insertion d'un autre modèle dans le modèle principal à l'aide de la balise `{include}`, qui contient des extraits, il est nécessaire d'envelopper à nouveau le modèle inclus dans une balise `snippetArea` et d'invalider à la fois l'extrait et la zone : ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* inclus.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -Vous pouvez également la combiner avec des extraits dynamiques. +Les snippets dans les composants .[#toc-snippets-in-components] +--------------------------------------------------------------- -Ajout et suppression .[#toc-adding-and-deleting] -================================================ - -Si vous ajoutez un nouvel élément dans la liste et que vous invalidez `itemsContainer`, la requête AJAX renvoie des extraits incluant le nouvel élément, mais le gestionnaire javascript ne sera pas en mesure de le rendre. Cela est dû au fait qu'il n'y a pas d'élément HTML avec l'ID nouvellement créé. - -Dans ce cas, le moyen le plus simple est d'envelopper toute la liste dans un autre extrait et de l'invalider : +Vous pouvez créer des snippets dans les [composants |components], et Nette les redessinera automatiquement. Cependant, il y a une limitation spécifique : pour redessiner les snippets, il faut appeler la méthode `render()` sans aucun paramètre. Par conséquent, passer des paramètres dans le modèle ne fonctionnera pas : ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Envoi de données utilisateur .[#toc-sending-user-data] +------------------------------------------------------ + +En plus des snippets, vous pouvez envoyer des données supplémentaires au client. Il suffit de les écrire dans l'objet `payload`: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + //... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -Il en va de même pour la suppression d'un élément. Il serait possible d'envoyer un extrait vide, mais les listes peuvent généralement être paginées et il serait compliqué d'implémenter la suppression d'un élément et le chargement d'un autre (qui se trouvait sur une page différente de la liste paginée). - -Envoi de paramètres au composant .[#toc-sending-parameters-to-component] -======================================================================== +Paramètres d'envoi .[#toc-sending-parameters] +============================================= -Lorsque nous envoyons des paramètres au composant via une requête AJAX, qu'il s'agisse de paramètres de signal ou de paramètres persistants, nous devons fournir leur nom global, qui contient également le nom du composant. Le nom global du paramètre renvoie la méthode `getParameterId()`. +Lorsque nous envoyons des paramètres au composant via une requête AJAX, qu'il s'agisse de paramètres de signal ou de paramètres persistants, nous devons fournir leur nom global, qui contient également le nom du composant. Le nom complet du paramètre renvoie la méthode `getParameterId()`. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -Et traiter la méthode avec les paramètres correspondants dans le composant. +Une méthode handle avec les paramètres correspondants dans le composant : ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/fr/bootstrap.texy b/application/fr/bootstrap.texy index 8bea659e34..06ae7a0019 100644 --- a/application/fr/bootstrap.texy +++ b/application/fr/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + private Configurator $configurator; + private string $rootDir; + + public function __construct() + { + $this->rootDir = dirname(__DIR__); + // Le configurateur est chargé de configurer l'environnement et les services de l'application. + $this->configurator = new Configurator; + // Définir le répertoire pour les fichiers temporaires générés par Nette (par exemple, les modèles compilés) + $this->configurator->setTempDirectory($this->rootDir . '/temp'); + } + + public function bootWebApplication(): Nette\DI\Container + { + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); + } + + private function initializeEnvironment(): void { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // Nette est intelligent, et le mode développement est activé automatiquement, + // ou vous pouvez l'activer pour une adresse IP spécifique en décommentant la ligne suivante: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Active Tracy: l'ultime outil de débogage "couteau suisse". + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: charge automatiquement toutes les classes dans le répertoire donné + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Chargement des fichiers de configuration + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php .[#toc-index-php] =========================== -Dans le cas des applications web, le fichier initial est `index.php`, qui se trouve dans le répertoire public `www/`. Il laisse la classe `Bootstrap` pour initialiser l'environnement et retourne la classe `$configurator` qui crée le conteneur DI. Ensuite, il obtient le service `Application`, qui exécute l'application web : +Dans le cas des applications web, le fichier principal est `index.php`, qui se trouve dans le [répertoire public |directory-structure#public-directory-www] `www/`. La classe Bootstrap initialisera l'environnement et produira un conteneur DI. Elle en tire ensuite le service `Application`, qui démarre l'application web : ```php -// initialisation de l'environnement + obtention de l'objet Configurateur -$configurator = App\Bootstrap::boot(); -// créer un conteneur DI -$container = $configurator->createContainer(); +$bootstrap = new App\Bootstrap; +// Initialiser l'environnement + créer un conteneur DI +$container = $bootstrap->bootWebApplication(); // Le conteneur DI crée un objet Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -// Démarrage de l'application Nette +// Démarrer l'application Nette et traiter la demande entrante $application->run(); ``` @@ -59,26 +84,42 @@ Comme vous pouvez le constater, la classe [api:Nette\Bootstrap\Configurator], qu Mode développement et mode production .[#toc-development-vs-production-mode] ============================================================================ -Nette distingue deux modes de base dans lesquels une requête est exécutée : développement et production. Le mode développement est axé sur le confort maximal du programmeur, Tracy est affiché, le cache est automatiquement mis à jour lors de la modification des modèles ou de la configuration du conteneur DI, etc. Le mode production est axé sur les performances, Tracy ne consigne que les erreurs et les modifications des templates et autres fichiers ne sont pas vérifiées. +Nette se comporte différemment selon qu'il fonctionne sur un serveur de développement ou de production : + +🛠️ Mode de développement: + - Affiche la barre de débogage de Tracy avec des informations utiles (par exemple, les requêtes SQL, le temps d'exécution, l'utilisation de la mémoire). + - Affiche une page d'erreur détaillée avec les traces des appels de fonction et le contenu des variables lorsqu'une erreur se produit. + - Actualise automatiquement le cache lorsque les modèles Latte, les fichiers de configuration, etc. sont modifiés. + + +Mode production: + - N'affiche aucune information de débogage ; toutes les erreurs sont enregistrées. + - Affiche une page `ErrorPresenter` ou une page générique "Server Error" lorsqu'une erreur se produit. + - Le cache n'est jamais actualisé automatiquement ! + - Optimisé pour la vitesse et la sécurité. -La sélection du mode se fait par autodétection, il n'est donc généralement pas nécessaire de configurer ou de changer quoi que ce soit manuellement. Le mode est développement si l'application est exécutée sur l'hôte local (c'est-à-dire l'adresse IP `127.0.0.1` ou `::1`) et qu'aucun proxy n'est présent (c'est-à-dire son en-tête HTTP). Sinon, elle s'exécute en mode production. + +Le mode est déterminé automatiquement, de sorte que dans la plupart des cas, il n'est pas nécessaire de le configurer ou de le changer manuellement : + +- Mode développement : Actif sur l'hôte local (adresse IP `127.0.0.1` ou `::1`) à moins qu'un proxy ne soit utilisé (d'après ses en-têtes HTTP). +- Mode production : Actif partout ailleurs. Si vous souhaitez activer le mode développement dans d'autres cas, par exemple pour les programmeurs accédant depuis une adresse IP spécifique, vous pouvez utiliser `setDebugMode()`: ```php -$configurator->setDebugMode('23.75.345.200'); // une ou plusieurs adresses IP +$this->configurator->setDebugMode('23.75.345.200'); // une ou plusieurs adresses IP ``` Nous recommandons vivement de combiner une adresse IP avec un cookie. Nous stockerons un jeton secret dans le cookie `nette-debug`, par exemple `secret1234`, et le mode de développement sera activé pour les programmeurs avec cette combinaison d'IP et de cookie. ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` Nous pouvons également désactiver complètement le mode de développement, même pour localhost : ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` Notez que la valeur `true` active le mode développeur par défaut, ce qui ne devrait jamais arriver sur un serveur de production. @@ -90,7 +131,7 @@ Outil de débogage Tracy .[#toc-debugging-tool-tracy] Pour faciliter le débogage, nous allons activer l'excellent outil [Tracy |tracy:]. En mode développeur, il visualise les erreurs et en mode production, il enregistre les erreurs dans le répertoire spécifié : ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ Fichiers temporaires .[#toc-temporary-files] Nette utilise le cache pour le conteneur DI, RobotLoader, les modèles, etc. Il est donc nécessaire de définir le chemin d'accès au répertoire où le cache sera stocké : ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` Sous Linux ou macOS, définissez les [droits d'écriture |nette:troubleshooting#Setting directory permissions] pour les répertoires `log/` et `temp/`. @@ -112,7 +153,7 @@ RobotLoader .[#toc-robotloader] En général, nous voulons charger automatiquement les classes à l'aide de [RobotLoader |robot-loader:], nous devons donc le lancer et le laisser charger les classes du répertoire où se trouve `Bootstrap.php` (c'est-à-dire `__DIR__`) et de tous ses sous-répertoires : ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ Fuseau horaire .[#toc-timezone] Le configurateur vous permet de spécifier un fuseau horaire pour votre application. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ En mode développement, le conteneur est automatiquement mis à jour chaque fois Les fichiers de configuration sont chargés à l'aide de `addConfig()`: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` La méthode `addConfig()` peut être appelée plusieurs fois pour ajouter plusieurs fichiers. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ Paramètres statiques .[#toc-static-parameters] Les paramètres utilisés dans les fichiers de configuration peuvent être définis [dans la section `parameters` |dependency-injection:configuration#parameters] et également transmis (ou écrasés) par la méthode `addStaticParameters()` (qui a un alias `addParameters()`). Il est important que les différentes valeurs des paramètres entraînent la génération de conteneurs DI supplémentaires, c'est-à-dire de classes supplémentaires. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ Paramètres dynamiques .[#toc-dynamic-parameters] Nous pouvons également ajouter des paramètres dynamiques au conteneur, leurs différentes valeurs, contrairement aux paramètres statiques, ne provoqueront pas la génération de nouveaux conteneurs DI. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ Les variables d'environnement peuvent être facilement mises à disposition à l'aide de paramètres dynamiques. Nous pouvons y accéder via `%env.variable%` dans les fichiers de configuration. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ Vous pouvez utiliser les paramètres statiques suivants dans les fichiers de con - `%wwwDir%` est le chemin absolu vers le répertoire contenant le fichier d'entrée `index.php` - `%tempDir%` est le chemin absolu vers le répertoire des fichiers temporaires - `%vendorDir%` est le chemin absolu vers le répertoire où Composer installe les bibliothèques +- `%rootDir%` est le chemin absolu vers le répertoire racine du projet - `%debugMode%` indique si l'application est en mode débogage - `%consoleMode%` indique si la demande provient de la ligne de commande @@ -225,7 +268,7 @@ services: Créez une nouvelle instance et insérez-la dans bootstrap : ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Différents environnements .[#toc-different-environments] ======================================================== -N'hésitez pas à personnaliser la classe `Bootstrap` en fonction de vos besoins. Vous pouvez ajouter des paramètres à la méthode `boot()` pour différencier les projets Web, ou ajouter d'autres méthodes, comme `bootForTests()`, qui initialise l'environnement pour les tests unitaires, `bootForCli()` pour les scripts appelés depuis la ligne de commande, etc. +N'hésitez pas à personnaliser la classe `Bootstrap` en fonction de vos besoins. Vous pouvez ajouter des paramètres à la méthode `bootWebApplication()` pour différencier les projets web. Vous pouvez également ajouter d'autres méthodes, telles que `bootTestEnvironment()` pour initialiser l'environnement des tests unitaires, `bootConsoleApplication()` pour les scripts appelés à partir de la ligne de commande, etc. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container +{ + Tester\Environment::setup(); // Initialisation du testeur Nette + $this->setupContainer(); + return $this->configurator->createContainer(); +} + +public function bootConsoleApplication(): Nette\DI\Container { - $configurator = self::boot(); - Tester\Environment::setup(); // Initialisation du testeur de nappe - return $configurator; + $this->configurator->setDebugMode(false); + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); } ``` diff --git a/application/fr/components.texy b/application/fr/components.texy index f2bee8ea59..a9472f1af9 100644 --- a/application/fr/components.texy +++ b/application/fr/components.texy @@ -230,6 +230,28 @@ Dans le modèle, ces messages sont disponibles dans la variable `$flashes` sous ``` +Redirection après un signal .[#toc-redirection-after-a-signal] +============================================================== + +Le traitement d'un signal de composant est souvent suivi d'une redirection. Cette situation est similaire à celle des formulaires : après avoir soumis un formulaire, nous redirigeons également les données pour éviter qu'elles ne soient soumises à nouveau lorsque la page est rafraîchie dans le navigateur. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Étant donné qu'un composant est un élément réutilisable et qu'il ne doit généralement pas dépendre directement de présentateurs spécifiques, les méthodes `redirect()` et `link()` interprètent automatiquement le paramètre comme un signal de composant : + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +Si vous devez rediriger vers un autre présentateur ou une autre action, vous pouvez le faire par l'intermédiaire du présentateur : + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Paramètres persistants .[#toc-persistent-parameters] ==================================================== @@ -347,7 +369,7 @@ services: Enfin, nous allons utiliser cette fabrique dans notre présentateur : ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ Les composants en profondeur .[#toc-components-in-depth] Les composants d'une application Nette sont les parties réutilisables d'une application Web que nous intégrons dans les pages, ce qui est le sujet de ce chapitre. Quelles sont exactement les capacités d'un tel composant ? 1) il peut être rendu dans un modèle -2) Il sait quelle partie de lui-même doit être rendue lors d'une [requête AJAX |ajax#invalidation] (snippets). +2) il sait [quelle partie de lui-même |ajax#snippets] rendre lors d'une requête AJAX (snippets) 3) il a la capacité de stocker son état dans une URL (paramètres persistants) 4) il a la capacité de répondre aux actions de l'utilisateur (signaux) 5) il crée une structure hiérarchique (dont la racine est le présentateur). diff --git a/application/fr/configuration.texy b/application/fr/configuration.texy index b358ce1617..5a6f9e1182 100644 --- a/application/fr/configuration.texy +++ b/application/fr/configuration.texy @@ -13,11 +13,15 @@ application: # affiche le panneau "Nette Application" dans Tracy BlueScreen ? debugger: ... # (bool) par défaut à true - # le présentateur d'erreur sera-t-il appelé en cas d'erreur ? - catchExceptions: ... # (bool) vaut true par défaut en mode production + # le présentateur d'erreurs sera-t-il appelé en cas d'erreur ? + # n'a d'effet qu'en mode développeur +catchExceptions : ... # (bool) valeur par défaut : true # nom du présentateur d'erreur - errorPresenter: Error # (string) vaut par défaut 'Nette:Error'. + errorPresenter: Error # (string|array) vaut par défaut 'Nette:Error'. + + # définit des alias pour les présentateurs et les événements + aliases: ... # définit les règles pour résoudre le nom du présentateur vers une classe mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) valeur par défaut: false ``` -Comme les présentateurs d'erreurs ne sont pas appelés par défaut en mode développement et que les erreurs sont affichées par Tracy, le fait de changer la valeur `catchExceptions` en `true` permet de vérifier que les présentateurs d'erreurs fonctionnent correctement pendant le développement. +Depuis la version 3.2 de `nette/application`, il est possible de définir une paire de présentateurs d'erreurs : + +```neon +application: + errorPresenter: + 4xx: Error4xx # pour NetteApplicationBadRequestException + 5xx: Error5xx # pour les autres exceptions +``` L'option `silentLinks` détermine comment Nette se comporte en mode développeur lorsque la génération de liens échoue (par exemple, parce qu'il n'y a pas de présentateur, etc). La valeur par défaut `false` signifie que Nette déclenche `E_USER_WARNING`. Le réglage sur `true` supprime ce message d'erreur. Dans un environnement de production, `E_USER_WARNING` est toujours invoqué. Nous pouvons également influencer ce comportement en définissant la variable du présentateur [$invalidLinkMode |creating-links#Invalid Links]. -Le [mappage définit les règles |modules#mapping] selon lesquelles le nom de la classe est dérivé du nom du présentateur. +Les [alias simplifient le référencement des |creating-links#aliases] présentateurs fréquemment utilisés. + +La [correspondance définit les règles |directory-structure#Presenter Mapping] selon lesquelles le nom de la classe est dérivé du nom du présentateur. Enregistrement automatique des présentateurs .[#toc-automatic-registration-of-presenters] @@ -77,10 +90,13 @@ latte: strictTypes: ... # (bool) vaut false par défaut # active le [mode strict de l'analyseur |latte:develop#strict mode] - strictParsing : ... # (bool) la valeur par défaut est false + strictParsing: ... # (bool) la valeur par défaut est false # permet de [vérifier le code généré |latte:develop#Checking Generated Code] - phpLinter : ... # (string) la valeur par défaut est null + phpLinter: ... # (string) la valeur par défaut est null + + # définit la locale + locale: cs_CZ # (string) la valeur par défaut est null # classe de $this->template templateClass: App\MyTemplateClass # Valeur par défaut: Nette\Bridges\ApplicationLatte\DefaultTemplate @@ -91,7 +107,7 @@ Si vous utilisez la version 3 de Latte, vous pouvez ajouter une nouvelle [extens ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` /--comment diff --git a/application/fr/creating-links.texy b/application/fr/creating-links.texy index c6786ac362..77264f560d 100644 --- a/application/fr/creating-links.texy +++ b/application/fr/creating-links.texy @@ -38,7 +38,7 @@ Il est également possible de passer des paramètres nommés. Le lien suivant pa detail ``` -Si la méthode `ProductPresenter::renderShow()` n'a pas `$lang` dans sa signature, elle peut lire la valeur du paramètre en utilisant `$lang = $this->getParameter('lang')`. +Si la méthode `ProductPresenter::renderShow()` n'a pas `$lang` dans sa signature, elle peut récupérer la valeur du paramètre à l'aide de `$lang = $this->getParameter('lang')` ou de la [propriété |presenters#Request Parameters]. Si les paramètres sont stockés dans un tableau, ils peuvent être développés avec l'opérateur `...` (ou `(expand)` dans Latte 2.x) : @@ -103,7 +103,7 @@ Si l'action est `default`, nous pouvons l'omettre, mais les deux points doivent home ``` -Les liens peuvent également pointer vers d'autres [modules]. Ici, on distingue les liens relatifs aux sous-modules et les liens absolus. Le principe est analogue à celui des chemins d'accès aux disques, mais les deux-points remplacent les barres obliques. Supposons que le présentateur actuel fasse partie du module `Front`, nous écrirons alors : +Les liens peuvent également pointer vers d'autres [modules |directory-structure#Presenters and Templates]. Dans ce cas, on distingue les liens relatifs aux sous-modules et les liens absolus. Le principe est analogue à celui des chemins d'accès aux disques, à ceci près que les barres obliques sont remplacées par des deux points. Supposons que le présentateur actuel fasse partie du module `Front`, nous écrirons alors : ```latte link to Front:Shop:Product:show @@ -140,7 +140,7 @@ La cible `this` créera un lien vers la page actuelle : refresh ``` -En même temps, tous les paramètres spécifiés dans la signature de l'instruction `render()` ou `action()` sont transférés. Ainsi, si nous sommes sur les pages `Product:show` et `id:123`, le lien vers `this` transmettra également ce paramètre. +En même temps, tous les paramètres spécifiés dans la signature de l'élément `action()` ou `render()` si la méthode `action()` n'est pas défini, sont transférés. Ainsi, si nous nous trouvons sur les pages `Product:show` et `id:123`, le lien vers `this` transmettra également ce paramètre. Bien sûr, il est possible de spécifier les paramètres directement : @@ -213,7 +213,7 @@ Comme les [composants |components] sont des unités distinctes réutilisables qu Si nous voulons créer un lien vers les présentateurs dans le modèle de composant, nous utilisons la balise `{plink}`: ```latte -home +home ``` ou dans le code @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Alias .[#toc-aliases]{data-version:v3.2.2} +========================================== + +Il est parfois utile d'attribuer un alias facilement mémorisable à une paire Présentateur:action. Par exemple, vous pouvez nommer la page d'accueil `Front:Home:default` simplement `home` ou `Admin:Dashboard:default` `admin` . + +Les alias sont définis dans la [configuration |configuration] sous la clé `application › aliases`: + +```neon +application: + aliases: + home: Front:Home:default + admin: Admin:Dashboard:default + sign: Front:Sign:in +``` + +Dans les liens, ils sont écrits en utilisant le symbole at, par exemple : + +```latte +administration +``` + +Ils sont pris en charge dans toutes les méthodes qui fonctionnent avec des liens, telles que `redirect()` et similaires. + + Liens non valides .[#toc-invalid-links] ======================================= @@ -257,6 +281,6 @@ Comment créer des liens avec le confort de la méthode `link()`, mais sans la p LinkGenerator est un service que vous pouvez faire passer par le constructeur et ensuite créer des liens en utilisant sa méthode `link()`. -Il y a une différence par rapport aux présentateurs. LinkGenerator crée tous les liens sous forme d'URL absolus. De plus, il n'y a pas de "présentateur actuel", il n'est donc pas possible de spécifier uniquement le nom de l'action `link('default')` ou les chemins relatifs vers les [modules]. +Par rapport aux présentateurs, il y a une différence. LinkGenerator crée tous les liens directement en tant qu'URL absolues. De plus, il n'y a pas de "présentateur réel", de sorte que vous ne pouvez pas simplement indiquer le nom de l'action `link('default')` comme cible ou énumérer les chemins relatifs vers les modules. Les liens non valides provoquent toujours une erreur à l'adresse `Nette\Application\UI\InvalidLinkException`. diff --git a/application/fr/directory-structure.texy b/application/fr/directory-structure.texy new file mode 100644 index 0000000000..7efc0d6dc9 --- /dev/null +++ b/application/fr/directory-structure.texy @@ -0,0 +1,526 @@ +Structure des répertoires de l'application +****************************************** + +
    + +Comment concevoir une structure de répertoires claire et évolutive pour les projets dans Nette Framework ? Nous vous montrerons des pratiques éprouvées qui vous aideront à organiser votre code. Vous apprendrez : + +- comment **structurer logiquement** l'application en répertoires +- comment concevoir la structure pour qu'elle **s'adapte bien** à la croissance du projet +- quelles sont les **alternatives possibles** et leurs avantages ou inconvénients + +
    + + +Il est important de mentionner que Nette Framework lui-même n'insiste pas sur une structure spécifique. Il est conçu pour s'adapter facilement à tous les besoins et à toutes les préférences. + + +Structure de base du projet .[#toc-basic-project-structure] +=========================================================== + +Bien que Nette Framework ne dicte pas de structure de répertoire fixe, il existe un arrangement par défaut éprouvé sous la forme d'un [projet Web : |https://github.com/nette/web-project] + +/--pre +web-project/ +├── app/ ← répertoire de l'application +├── assets/ ← fichiers SCSS, JS, images..., alternativement ressources/ +├── bin/ ← scripts de ligne de commande +├── config/ ← configuration +├── log/ ← erreurs enregistrées +├── temp/ ← fichiers temporaires, cache +├── tests/ ← tests +├── vendor/ ← bibliothèques installées par Composer +└── www/ ← répertoire public (document-root) +\-- + +Vous pouvez librement modifier cette structure en fonction de vos besoins - renommer ou déplacer des dossiers. Il vous suffit alors d'ajuster les chemins relatifs des répertoires dans `Bootstrap.php` et éventuellement `composer.json`. Rien d'autre n'est nécessaire, pas de reconfiguration complexe, pas de changements constants. Nette dispose d'une autodétection intelligente et reconnaît automatiquement l'emplacement de l'application, y compris sa base d'URL. + + +Principes d'organisation du code .[#toc-code-organization-principles] +===================================================================== + +Lorsque vous explorez un nouveau projet pour la première fois, vous devez être en mesure de vous orienter rapidement. Imaginez que vous cliquez sur le répertoire `app/Model/` et que vous voyez cette structure : + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +Vous apprendrez seulement que le projet utilise certains services, référentiels et entités. Vous n'apprendrez rien sur l'objectif réel de l'application. + +Examinons une approche différente - **l'organisation par domaines** : + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +C'est différent - au premier coup d'œil, il est clair qu'il s'agit d'un site de commerce électronique. Les noms des répertoires eux-mêmes révèlent ce que l'application peut faire - elle fonctionne avec des paiements, des commandes et des produits. + +La première approche (organisation par type de classe) pose plusieurs problèmes dans la pratique : le code qui est logiquement lié est dispersé dans différents dossiers et vous devez passer de l'un à l'autre. C'est pourquoi nous allons l'organiser par domaines. + + +Espaces de noms .[#toc-namespaces] +---------------------------------- + +Il est conventionnel que la structure des répertoires corresponde aux espaces de noms dans l'application. Cela signifie que l'emplacement physique des fichiers correspond à leur espace de noms. Par exemple, une classe située dans `app/Model/Product/ProductRepository.php` devrait avoir l'espace de noms `App\Model\Product`. Ce principe facilite l'orientation du code et simplifie l'autoloading. + + +Singulier et pluriel dans les noms .[#toc-singular-vs-plural-in-names] +---------------------------------------------------------------------- + +Remarquez que nous utilisons le singulier pour les principaux répertoires d'applications : `app`, `config`, `log`, `temp`, `www`. Il en va de même à l'intérieur de l'application : `Model`, `Core`, `Presentation`. Cela s'explique par le fait que chacun représente un concept unifié. + +De même, `app/Model/Product` représente tout ce qui concerne les produits. Nous ne l'appelons pas `Products` parce qu'il ne s'agit pas d'un dossier rempli de produits (qui contiendrait des fichiers comme `iphone.php`, `samsung.php`). Il s'agit d'un espace de noms contenant des classes permettant de travailler avec des produits - `ProductRepository.php`, `ProductService.php`. + +Le dossier `app/Tasks` est pluriel parce qu'il contient un ensemble de scripts exécutables autonomes - `CleanupTask.php`, `ImportTask.php`. Chacun d'entre eux est une unité indépendante. + +Par souci de cohérence, nous recommandons d'utiliser : +- le singulier pour les espaces de noms représentant une unité fonctionnelle (même si l'on travaille avec plusieurs entités) +- Pluriel pour les collections d'unités indépendantes +- En cas d'incertitude ou si vous ne voulez pas y penser, choisissez le singulier + + +Répertoire public `www/` .[#toc-public-directory-www] +===================================================== + +Ce répertoire est le seul accessible depuis le web (ce qu'on appelle le document-root). Vous rencontrerez souvent le nom `public/` au lieu de `www/` - c'est juste une question de convention et n'affecte pas la fonctionnalité. Le répertoire contient +- [Point d'entrée de l' |bootstrap#index.php] application `index.php` +- le fichier `.htaccess` avec les règles mod_rewrite (pour Apache) +- Fichiers statiques (CSS, JavaScript, images) +- Fichiers téléchargés + +Pour assurer la sécurité de l'application, il est essentiel que la [racine du document |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url] soit correctement [configurée |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url]. + +.[note] +Ne placez jamais le dossier `node_modules/` dans ce répertoire - il contient des milliers de fichiers qui peuvent être exécutables et ne doivent pas être accessibles au public. + + +Répertoire des applications `app/` .[#toc-application-directory-app] +==================================================================== + +Il s'agit du répertoire principal contenant le code de l'application. Structure de base : + +/--pre +app/ +├── Core/ ← questions d'infrastructure +├── Model/ ← logique d'entreprise +├── Presentation/ ← présentateurs et modèles +├── Tasks/ ← scripts de commande +└── Bootstrap.php ← classe d'amorçage d'application +\-- + +`Bootstrap.php` est la [classe de démarrage de l'application |bootstrap] qui initialise l'environnement, charge la configuration et crée le conteneur DI. + +Examinons maintenant les différents sous-répertoires en détail. + + +Présentateurs et modèles .[#toc-presenters-and-templates] +========================================================= + +La partie présentation de l'application se trouve dans le répertoire `app/Presentation`. Une alternative est le répertoire court `app/UI`. C'est l'endroit où se trouvent tous les présentateurs, leurs modèles et toutes les classes d'aide. + +Nous organisons cette couche par domaines. Dans un projet complexe qui combine le commerce électronique, le blog et l'API, la structure ressemblerait à ceci : + +/--pre +app/Presentation/ +├── Shop/ ← e-commerce frontend +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← blog +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administration +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← points d'extrémité API + └── V1/ +\-- + +À l'inverse, pour un simple blog, nous utiliserions la structure suivante : + +/--pre +app/Presentation/ +├── Front/ ← site web frontend +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administration +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, sitemaps, etc. +\-- + +Les dossiers tels que `Home/` ou `Dashboard/` contiennent les présentateurs et les modèles. Les dossiers tels que `Front/`, `Admin/` ou `Api/` sont appelés **modules**. Techniquement, il s'agit de répertoires ordinaires qui servent à l'organisation logique de l'application. + +Chaque dossier contenant un présentateur contient un présentateur de même nom et ses modèles. Par exemple, le dossier `Dashboard/` contient : + +/--pre +Dashboard/ +├── DashboardPresenter.php ← présentateur +└── default.latte ← modèle +\-- + +Cette structure de répertoire se reflète dans les espaces de noms des classes. Par exemple, `DashboardPresenter` se trouve dans l'espace de noms `App\Presentation\Admin\Dashboard` (voir le [mappage des présentateurs |#presenter mapping]) : + +```php +namespace App\Presentation\Admin\Dashboard; + +class DashboardPresenter extends Nette\Application\UI\Presenter +{ + //... +} +``` + +Nous faisons référence au présentateur `Dashboard` à l'intérieur du module `Admin` dans l'application en utilisant la notation des deux points comme `Admin:Dashboard`. À son action `default`, nous nous référons alors à `Admin:Dashboard:default`. Pour les modules imbriqués, nous utilisons plus de deux points, par exemple `Shop:Order:Detail:default`. + + +Développement d'une structure flexible .[#toc-flexible-structure-development] +----------------------------------------------------------------------------- + +L'un des grands avantages de cette structure est qu'elle s'adapte élégamment aux besoins croissants des projets. Prenons l'exemple de la partie qui génère des flux XML. Au départ, nous avons un simple formulaire : + +/--pre +Export/ +├── ExportPresenter.php ← un seul présentateur pour toutes les exportations +├── sitemap.latte ← modèle de plan du site +└── feed.latte ← modèle pour flux RSS +\-- + +Au fil du temps, d'autres types de flux sont ajoutés et nous avons besoin de plus de logique pour eux... Pas de problème ! Le dossier `Export/` devient simplement un module : + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← flux pour Amazon + └── ebay.latte ← flux pour eBay +\-- + +Cette transformation se fait en douceur : il suffit de créer de nouveaux sous-dossiers, d'y répartir le code et de mettre à jour les liens (par exemple, de `Export:feed` à `Export:Feed:amazon`). Grâce à cela, nous pouvons progressivement étendre la structure en fonction des besoins, le niveau d'imbrication n'étant en aucun cas limité. + +Par exemple, si dans l'administration vous avez de nombreux présentateurs liés à la gestion des commandes, tels que `OrderDetail`, `OrderEdit`, `OrderDispatch` etc., vous pouvez créer un module (dossier) `Order` pour une meilleure organisation, qui contiendra (des dossiers pour) les présentateurs `Detail`, `Edit`, `Dispatch` et d'autres. + + +Emplacement du modèle .[#toc-template-location] +----------------------------------------------- + +Dans les exemples précédents, nous avons vu que les modèles sont situés directement dans le dossier du présentateur : + +/--pre +Dashboard/ +├── DashboardPresenter.php ← présentateur +├── DashboardTemplate.php ← classe de modèle optionnelle +└── default.latte ← modèle +\-- + +Cet emplacement s'avère être le plus pratique dans la pratique - vous avez tous les fichiers connexes à portée de main. + +Vous pouvez également placer les modèles dans un sous-dossier `templates/`. Nette prend en charge les deux variantes. Vous pouvez même placer les modèles complètement en dehors du dossier `Presentation/`. Tout ce qui concerne les options d'emplacement des modèles se trouve dans le chapitre [Consultation des modèles |templates#Template Lookup]. + + +Classes d'aide et composants .[#toc-helper-classes-and-components] +------------------------------------------------------------------ + +Les présentateurs et les modèles sont souvent accompagnés d'autres fichiers d'aide. Nous les plaçons logiquement en fonction de leur portée : + +1. **Directement avec le présentateur** dans le cas de composants spécifiques pour le présentateur donné : + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← composant pour la liste des produits +└── FilterForm.php ← formulaire de filtrage +\-- + +2. **Pour le module** - nous recommandons d'utiliser le dossier `Accessory`, qui est placé proprement au début de l'alphabet : + +/--pre +Front/ +├── Accessory/ +│ ├── NavbarControl.php ← composants pour le frontend +│ └── TemplateFilters.php +├── Product/ +└── Cart/ +\-- + +3. **Pour l'ensemble de l'application** - dans `Presentation/Accessory/`: +/--pre +app/Presentation/ +├── Accessory/ +│ ├── LatteExtension.php +│ └── TemplateFilters.php +├── Front/ +└── Admin/ +\-- + +Vous pouvez également placer des classes d'aide comme `LatteExtension.php` ou `TemplateFilters.php` dans le dossier d'infrastructure `app/Core/Latte/`. Et les composants dans `app/Components`. Le choix dépend des conventions de l'équipe. + + +Modèle - Cœur de l'application .[#toc-model-heart-of-the-application] +===================================================================== + +Le modèle contient toute la logique commerciale de l'application. Pour son organisation, la même règle s'applique - nous structurons par domaines : + +/--pre +app/Model/ +├── Payment/ ← tout sur les paiements +│ ├── PaymentFacade.php ← point d'entrée principal +│ ├── PaymentRepository.php +│ ├── Payment.php ← entité +├── Order/ ← tout sur les commandes +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← tout sur l'expédition +\-- + +Dans le modèle, vous rencontrez typiquement ces types de classes : + +**Facades** : elles représentent le principal point d'entrée dans un domaine spécifique de l'application. Elles agissent comme un orchestrateur qui coordonne la coopération entre différents services pour mettre en œuvre des cas d'utilisation complets (comme "créer une commande" ou "traiter un paiement"). Sous sa couche d'orchestration, la façade cache les détails de la mise en œuvre au reste de l'application, fournissant ainsi une interface propre pour travailler avec le domaine donné. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // validation + // création de commandes + // envoi de courriels + // écriture dans les statistiques + } +} +``` + +**Services** : ils se concentrent sur des opérations commerciales spécifiques au sein d'un domaine. Contrairement aux façades qui orchestrent des cas d'utilisation entiers, un service met en œuvre une logique commerciale spécifique (comme le calcul des prix ou le traitement des paiements). Les services sont généralement sans état et peuvent être utilisés soit par les façades comme blocs de construction pour des opérations plus complexes, soit directement par d'autres parties de l'application pour des tâches plus simples. + +```php +class PricingService +{ + public function calculateTotal(Order $order): Money + { + // calcul du prix + } +} +``` + +Les **référentiels** : gèrent toutes les communications avec le stockage des données, généralement une base de données. Leur tâche consiste à charger et à enregistrer des entités et à mettre en œuvre des méthodes de recherche. Un référentiel protège le reste de l'application des détails de la mise en œuvre de la base de données et fournit une interface orientée objet pour travailler avec les données. + +```php +class OrderRepository +{ + public function find(int $id): ?Order + { + } + + public function findByCustomer(int $customerId): array + { + } +} +``` + +**Entités** : objets représentant les principaux concepts commerciaux de l'application, qui ont leur identité et changent au fil du temps. Il s'agit généralement de classes mappées sur des tables de base de données à l'aide d'un ORM (comme Nette Database Explorer ou Doctrine). Les entités peuvent contenir des règles de gestion concernant leurs données et leur logique de validation. + +```php +// Entité associée à la table de la base de données "commandes". +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, + ]); + } +} +``` + +**Objets valeur** : objets immuables représentant des valeurs sans identité propre - par exemple, un montant d'argent ou une adresse électronique. Deux instances d'un objet valeur ayant les mêmes valeurs sont considérées comme identiques. + + +Code d'infrastructure .[#toc-infrastructure-code] +================================================= + +Le dossier `Core/` (ou également `Infrastructure/`) contient la base technique de l'application. Le code d'infrastructure comprend généralement + +/--pre +app/Core/ +├── Router/ ← routage et gestion des URL +│ └── RouterFactory.php +├── Security/ ← authentification et autorisation +│ ├── Authenticator.php +│ └── Authorizator.php +├── Logging/ ← journalisation et surveillance +│ ├── SentryLogger.php +│ └── FileLogger.php +├── Cache/ ← couche de mise en cache +│ └── FullPageCache.php +└── Integration/ ← intégration avec des services externes + ├── Slack/ + └── Stripe/ +\-- + +Pour les petits projets, une structure plate est naturellement suffisante : + +/--pre +Core/ +├── RouterFactory.php +├── Authenticator.php +└── QueueMailer.php +\-- + +C'est du code qui : + +- gère l'infrastructure technique (routage, journalisation, mise en cache) +- intègre des services externes (Sentry, Elasticsearch, Redis) +- Fournit des services de base pour l'ensemble de l'application (courrier, base de données) +- est en grande partie indépendant du domaine spécifique - le cache ou le logger fonctionne de la même manière pour un commerce électronique ou un blog. + +Vous vous demandez si une certaine classe a sa place ici ou dans le modèle ? La différence essentielle est que le code dans `Core/`: + +- ne connaît rien du domaine (produits, commandes, articles) +- peut généralement être transféré vers un autre projet +- Résout "comment ça marche" (comment envoyer du courrier), et non "ce que ça fait" (quel courrier envoyer). + +Exemple pour une meilleure compréhension : + +- `App\Core\MailerFactory` - crée des instances de la classe d'envoi de courrier électronique, gère les paramètres SMTP +- `App\Model\OrderMailer` - utilise `MailerFactory` pour envoyer des courriels sur les commandes, connaît leurs modèles et le moment où ils doivent être envoyés. + + +Scripts de commande .[#toc-command-scripts] +=========================================== + +Les applications ont souvent besoin d'effectuer des tâches en dehors des requêtes HTTP normales, qu'il s'agisse de traitement de données en arrière-plan, de maintenance ou de tâches périodiques. Les scripts simples du répertoire `bin/` sont utilisés pour l'exécution, tandis que la logique d'implémentation réelle est placée dans `app/Tasks/` (ou `app/Commands/`). + +Exemple : + +/--pre +app/Tasks/ +├── Maintenance/ ← scripts de maintenance +│ ├── CleanupCommand.php ← suppression d'anciennes données +│ └── DbOptimizeCommand.php ← optimisation de la base de données +├── Integration/ ← intégration avec des systèmes externes +│ ├── ImportProducts.php ← importation à partir du système du fournisseur +│ └── SyncOrders.php ← synchronisation des commandes +└── Scheduled/ ← tâches régulières + ├── NewsletterCommand.php ← envoi de lettres d'information + └── ReminderCommand.php ← notifications aux clients +\-- + +Qu'est-ce qui relève du modèle et qu'est-ce qui relève des scripts de commande ? Par exemple, la logique d'envoi d'un courriel fait partie du modèle, tandis que l'envoi en masse de milliers de courriels relève de `Tasks/`. + +Les tâches sont généralement [exécutées à partir de la ligne de commande |https://blog.nette.org/en/cli-scripts-in-nette-application] ou via cron. Elles peuvent également être exécutées via une requête HTTP, mais la sécurité doit être prise en compte. Le présentateur qui exécute la tâche doit être sécurisé, par exemple uniquement pour les utilisateurs connectés ou avec un jeton fort et un accès à partir d'adresses IP autorisées. Pour les tâches de longue durée, il est nécessaire d'augmenter la limite de temps du script et d'utiliser `session_write_close()` pour éviter de verrouiller la session. + + +Autres répertoires possibles .[#toc-other-possible-directories] +=============================================================== + +En plus des répertoires de base mentionnés, vous pouvez ajouter d'autres dossiers spécialisés en fonction des besoins du projet. Examinons les plus courants et leur utilisation : + +/--pre +app/ +├── Api/ ← logique de l'API indépendante de la couche de présentation +├── Database/ ← des scripts de migration et des semoirs pour les données de test +├── Components/ ← composants visuels partagés dans l'ensemble de l'application +├── Event/ ← utile en cas d'utilisation d'une architecture pilotée par les événements +├── Mail/ ← modèles de courrier électronique et logique connexe +└── Utils/ ← classes d'aide +\-- + +Pour les composants visuels partagés utilisés dans les présentateurs de l'application, vous pouvez utiliser le dossier `app/Components` ou `app/Controls`: + +/--pre +app/Components/ +├── Form/ ← composants de formulaires partagés +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← composants pour les listes de données +│ └── DataGrid.php +└── Navigation/ ← éléments de navigation + ├── Breadcrumbs.php + └── Menu.php +\-- + +C'est dans ce dossier que se trouvent les composants dont la logique est plus complexe. Si vous souhaitez partager des composants entre plusieurs projets, il est préférable de les séparer dans un package de composition autonome. + +Dans le répertoire `app/Mail`, vous pouvez placer la gestion de la communication par courriel : + +/--pre +app/Mail/ +├── templates/ ← modèles de courrier électronique +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Cartographie des présentateurs .[#toc-presenter-mapping] +======================================================== + +Le mappage définit des règles pour dériver les noms de classes des noms de présentateurs. Nous les spécifions dans la [configuration |configuration] sous la clé `application › mapping`. + +Sur cette page, nous avons montré que nous plaçons les présentateurs dans le dossier `app/Presentation` (ou `app/UI`). Nous devons informer Nette de cette convention dans le fichier de configuration. Une ligne suffit : + +```neon +application: + mapping: App\Presentation\*\**Presenter +``` + +Comment fonctionne le mapping ? Pour mieux comprendre, imaginons d'abord une application sans modules. Nous voulons que les classes de présentateurs relèvent de l'espace de noms `App\Presentation`, de sorte que le présentateur `Home` soit associé à la classe `App\Presentation\HomePresenter`. Cette configuration permet d'atteindre cet objectif : + +```neon +application: + mapping: App\Presentation\*Presenter +``` + +Le mappage s'effectue en remplaçant l'astérisque du masque `App\Presentation\*Presenter` par le nom du présentateur `Home`, ce qui donne le nom de classe final `App\Presentation\HomePresenter`. C'est simple ! + +Cependant, comme vous le verrez dans les exemples de ce chapitre et d'autres, nous plaçons les classes de présentateurs dans des sous-répertoires éponymes, par exemple le présentateur `Home` correspond à la classe `App\Presentation\Home\HomePresenter`. Pour ce faire, nous doublons les deux points (Nette Application 3.2 nécessaire) : + +```neon +application: + mapping: App\Presentation\**Presenter +``` + +Nous allons maintenant passer au mappage des présentateurs dans les modules. Nous pouvons définir un mappage spécifique pour chaque module : + +```neon +application: + mapping: + Front: App\Presentation\Front\**Presenter + Admin: App\Presentation\Admin\**Presenter + Api: App\Api\*Presenter +``` + +Selon cette configuration, le présentateur `Front:Home` correspond à la classe `App\Presentation\Front\Home\HomePresenter`, tandis que le présentateur `Api:OAuth` correspond à la classe `App\Api\OAuthPresenter`. + +Étant donné que les modules `Front` et `Admin` ont une méthode de mappage similaire et qu'il y aura probablement d'autres modules de ce type, il est possible de créer une règle générale qui les remplacera. Un nouvel astérisque pour le module sera ajouté au masque de classe : + +```neon +application: + mapping: + *: App\Presentation\*\**Presenter + Api: App\Api\*Presenter +``` + +Cela fonctionne également pour les structures de répertoires imbriqués plus profonds, comme le présentateur `Admin:User:Edit`, où le segment avec astérisque se répète pour chaque niveau et aboutit à la classe `App\Presentation\Admin\User\Edit\EditPresenter`. + +Une autre notation consiste à utiliser un tableau composé de trois segments au lieu d'une chaîne. Cette notation est équivalente à la précédente : + +```neon +application: + mapping: + *: [App\Presentation, *, **Presenter] + Api: [App\Api, '', *Presenter] +``` diff --git a/application/fr/how-it-works.texy b/application/fr/how-it-works.texy index 09d020332a..3947ac2de6 100644 --- a/application/fr/how-it-works.texy +++ b/application/fr/how-it-works.texy @@ -22,18 +22,18 @@ La structure des répertoires ressemble à ceci : /--pre web-project/ ├── app/ ← répertoire avec application -│ ├── Presenters/ ← classes d'presenter -│ │ ├── HomePresenter.php ← Home classe des présentateurs -│ │ └── templates/ ← répertoire des modèles -│ │ ├── @layout.latte ← modèle de disposition partagée -│ │ └── Home/ ← Modèles pour le présentateur de la page d'accueil -│ │ └── default.latte ← modèle pour l'action `default` -│ ├── Router/ ← configuration des adresses URL +│ ├── Core/ ← basic necessary classes +│ │ └── RouterFactory.php ← configuration des adresses URL +│ ├── Presentation/ ← presenters, templates & co. +│ │ ├─── @layout.latte ← template of shared layout +│ │ └── Home/ ← Home presenter directory +│ │ ├── HomePresenter.php ← Classe Home Presenter +│ │ └── default.latte ← template for action default │ └── Bootstrap.php ← classe de démarrage Bootstrap ├── bin/ ← scripts pour la ligne de commande ├── config/ ← configuration files │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← journaux d'erreurs ├── temp/ ← fichiers temporaires, cache, … ├── vendor/ ← bibliothèques installées par Composer @@ -45,9 +45,9 @@ La structure des répertoires ressemble à ceci : └── .htaccess ← interdit l'accès à tous les répertoires sauf www \-- -Vous pouvez modifier la structure des répertoires de n'importe quelle manière, renommer ou déplacer des dossiers, puis modifier simplement les chemins d'accès à `log/` et `temp/` dans le fichier `Bootstrap.php` et le chemin d'accès à ce fichier dans `composer.json` dans la section `autoload`. Rien de plus, pas de reconfiguration compliquée, pas de changements constants. Nette dispose d'une [autodétection intelligente |bootstrap#development-vs-production-mode]. +Vous pouvez modifier la structure du répertoire comme vous le souhaitez, renommer ou déplacer des dossiers - c'est totalement flexible. Nette est également doté d'une fonction d'autodétection intelligente et reconnaît automatiquement l'emplacement de l'application, y compris sa base URL. -Pour les applications un peu plus importantes, nous pouvons diviser les dossiers contenant des présentateurs et des modèles en sous-répertoires (sur le disque) et en espaces de noms (dans le code), que nous appelons [modules]. +Pour les applications un peu plus importantes, nous pouvons organiser les dossiers de présentateurs et de modèles en [sous-répertoires |directory-structure#Presenters and templates] et regrouper les classes dans des espaces de noms, que nous appelons modules. Le répertoire `www/` est le répertoire public ou la racine du document du projet. Vous pouvez le renommer sans avoir à définir quoi que ce soit d'autre du côté de l'application. Il suffit de [configurer l'hébergement |nette:troubleshooting#How to change or remove www directory from URL] pour que le document-root aille dans ce répertoire. @@ -75,7 +75,7 @@ Sa tâche est la suivante : Quel genre d'usine ? Nous ne produisons pas de tracteurs, mais des sites web ! Attendez, je vais vous expliquer tout de suite. -Par "initialiser l'environnement", nous voulons dire, par exemple, que [Tracy |tracy:] est activé, qui est un outil étonnant pour consigner ou visualiser les erreurs. Il enregistre les erreurs sur le serveur de production et les affiche directement sur le serveur de développement. Par conséquent, l'initialisation doit également décider si le site fonctionne en mode production ou en mode développement. Pour ce faire, Nette utilise l'autodétection : si vous exécutez le site sur localhost, il fonctionne en mode développeur. Vous n'avez rien à configurer et l'application est prête à être déployée aussi bien en développement qu'en production. Ces étapes sont réalisées et décrites en détail dans le chapitre sur la [classe Bootstrap |bootstrap]. +Par "initialisation de l'environnement", nous entendons, par exemple, l'activation de [Tracy |tracy:], qui est un outil fantastique pour la journalisation et la visualisation des erreurs. Sur les serveurs de production, il enregistre les erreurs, tandis que sur les serveurs de développement, il les affiche directement. L'initialisation consiste donc à déterminer si le site web fonctionne en mode production ou en mode développement. Pour cela, Nette utilise une [autodétection intelligente |bootstrap#development-vs-production-mode]: si vous exécutez le site sur localhost, il fonctionne en mode développement. Aucune configuration n'est nécessaire et l'application est prête à être déployée aussi bien en mode développement qu'en mode production. Ces étapes sont réalisées et détaillées dans le chapitre sur [la classe Bootstrap |bootstrap]. Le troisième point (oui, nous avons sauté le deuxième, mais nous y reviendrons) consiste à démarrer l'application. Le traitement des demandes HTTP dans Nette est effectué par la classe `Nette\Application\Application` (ci-après dénommée `Application`), donc lorsque nous disons "lancer une application", nous voulons dire appeler une méthode portant le nom `run()` sur un objet de cette classe. @@ -91,7 +91,7 @@ Les applications écrites dans Nette sont divisées en plusieurs présentateurs L'application commence par demander à ce qu'on appelle le routeur de décider lequel des présentateurs doit transmettre la demande actuelle pour traitement. Le routeur décide de la responsabilité qui lui incombe. Il examine l'URL d'entrée `https://example.com/product/123`, qui veut `show` un produit avec `id: 123` comme action. C'est une bonne habitude d'écrire une paire présentateur + action séparée par un deux-points comme `Product:show`. -Le routeur transforme donc l'URL en une paire `Presenter:action` + paramètres, dans notre cas `Product:show` + `id: 123`. Vous pouvez voir à quoi ressemble un routeur dans le fichier `app/Router/RouterFactory.php` et nous le décrirons en détail dans le chapitre [Routage |Routing]. +Le routeur transforme donc l'URL en une paire `Presenter:action` + paramètres, dans notre cas `Product:show` + `id: 123`. Vous pouvez voir à quoi ressemble un routeur dans le fichier `app/Core/RouterFactory.php` et nous le décrirons en détail dans le chapitre [Routage |Routing]. Continuons. L'application connaît déjà le nom du présentateur et peut continuer. En créant un objet `ProductPresenter`, qui est le code du présentateur `Product`. Plus précisément, elle demande au conteneur DI de créer le présentateur, car la production d'objets est son travail. @@ -121,12 +121,9 @@ Ainsi, la méthode `renderShow(123)` a été appelée, dont le code est un exemp Ensuite, le présentateur renvoie la réponse. Cela peut être une page HTML, une image, un document XML, l'envoi d'un fichier depuis le disque, JSON ou la redirection vers une autre page. Il est important de noter que si nous ne disons pas explicitement comment répondre (ce qui est le cas de `ProductPresenter`), la réponse sera de rendre le modèle avec une page HTML. Pourquoi ? Eh bien, parce que dans 99% des cas, nous voulons dessiner un modèle, donc le présentateur prend ce comportement par défaut et veut nous faciliter le travail. C'est le point de vue de Nette. -Nous n'avons même pas besoin d'indiquer quel modèle dessiner, il dérive le chemin vers celui-ci selon une logique simple. Dans le cas du présentateur `Product` et de l'action `show`, il essaie de voir si l'un de ces fichiers modèles existe par rapport au répertoire où se trouve la classe `ProductPresenter`: +Il n'est même pas nécessaire de spécifier le modèle à rendre ; le framework déduira lui-même le chemin d'accès. Dans le cas de l'action `show`, il essaie simplement de charger le modèle `show.latte` dans le répertoire contenant la classe `ProductPresenter`. Il tente également de trouver la mise en page dans le fichier `@layout.latte` (plus d'informations sur la [recherche de modèles |templates#Template Lookup]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -Il essaiera également de trouver la mise en page dans le fichier `@layout.latte`, puis il effectuera le rendu du modèle. La tâche du présentateur et de l'ensemble de l'application est maintenant terminée. Si le modèle n'existe pas, une page d'erreur 404 sera renvoyée. Vous pouvez en savoir plus sur les présentateurs sur la page [Présentateurs |Presenters]. +Ensuite, les modèles sont rendus. La tâche du présentateur et de l'ensemble de l'application est ainsi achevée et le travail est terminé. Si le modèle n'existait pas, une page d'erreur 404 serait renvoyée. Pour en savoir plus sur les présentateurs, consultez la page [Présentateurs |presenters]. [* request-flow.svg *] @@ -137,7 +134,7 @@ Juste pour être sûr, essayons de récapituler l'ensemble du processus avec une 3) le routeur décode l'URL comme une paire `Home:default` 4) un objet `HomePresenter` est créé 5) la méthode `renderDefault()` est appelée (si elle existe) -6) un modèle `templates/Home/default.latte` avec une mise en page `templates/@layout.latte` est rendu +6) un modèle `default.latte` avec une mise en page `@layout.latte` est rendu Vous avez peut-être rencontré beaucoup de nouveaux concepts maintenant, mais nous pensons qu'ils ont un sens. Créer des applications dans Nette est un jeu d'enfant. diff --git a/application/fr/modules.texy b/application/fr/modules.texy deleted file mode 100644 index ade58f61b9..0000000000 --- a/application/fr/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Modules -******* - -.[perex] -Dans Nette, les modules représentent les unités logiques qui composent une application. Ils comprennent des présentateurs, des modèles, éventuellement aussi des composants et des classes de modèles. - -Un répertoire pour les présentateurs et un autre pour les modèles ne seraient pas suffisants pour les projets réels. Avoir des dizaines de fichiers dans un seul dossier est pour le moins inorganisé. Comment s'en sortir ? Il suffit de les répartir en sous-répertoires sur le disque et en espaces de noms dans le code. Et c'est exactement ce que font les modules Nette. - -Oublions donc le dossier unique pour les présentateurs et les modèles et créons plutôt des modules, par exemple `Admin` et `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← répertoire avec les modules -│ ├── Admin/ ← module Admin -│ │ ├── Presenters/ ← ses présentateurs -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← module Front -│ └── Presenters/ ← ses présentateurs -│ └── ... -\-- - -Cette structure de répertoire sera reflétée par les espaces de noms des classes, ainsi par exemple `DashboardPresenter` sera dans l'espace de noms `App\Modules\Admin\Presenters`: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Le présentateur `Dashboard` dans le module `Admin` est référencé dans l'application en utilisant la notation deux points comme `Admin:Dashboard`, et son action `default` comme `Admin:Dashboard:default`. -Et comment Nette proper sait-elle que `Admin:Dashboard` représente la classe `App\Modules\Admin\Presenters\DashboardPresenter`? Cela est déterminé par le [mappage |#mapping] dans la configuration. -Ainsi, la structure donnée n'est pas figée et vous pouvez la modifier en fonction de vos besoins. - -Les modules peuvent bien sûr contenir tous les éléments autres que les présentateurs et les modèles, tels que les composants, les classes de modèles, etc. - - -Modules imbriqués .[#toc-nested-modules] ----------------------------------------- - -Les modules ne doivent pas uniquement former une structure plate, vous pouvez également créer des sous-modules, par exemple : - -/--pre -app/ -├── Modules/ ← répertoire avec les modules -│ ├── Blog/ ← module Blog -│ │ ├── Admin/ ← sous-module Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← sous-module Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← module Forum -│ │ └── ... -\-- - -Ainsi, le module `Blog` est divisé en sous-modules `Admin` et `Front`. Là encore, cela se reflétera dans les espaces de noms, qui seront `App\Modules\Blog\Admin\Presenters` etc. Le présentateur `Dashboard` à l'intérieur du sous-module est désigné par `Blog:Admin:Dashboard`. - -L'imbrication peut aller aussi loin que vous le souhaitez, de sorte que des sous-sous-modules peuvent être créés. - - -Création de liens .[#toc-creating-links] ----------------------------------------- - -Les liens dans les modèles de présentateur sont relatifs au module actuel. Ainsi, le lien `Foo:default` mène au présentateur `Foo` dans le même module que le présentateur actuel. Si le module actuel est `Front`, par exemple, le lien est le suivant : - -```latte -link to Front:Product:show -``` - -Un lien est relatif même s'il inclut le nom d'un module, qui est alors considéré comme un sous-module : - -```latte -link to Front:Shop:Product:show -``` - -Les liens absolus sont écrits de manière analogue aux chemins absolus sur le disque, mais avec des deux-points à la place des barres obliques. Ainsi, un lien absolu commence par un deux-points : - -```latte -link to Admin:Product:show -``` - -Pour savoir si nous sommes dans un certain module ou son sous-module, nous pouvons utiliser la fonction `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Acheminement .[#toc-routing] ----------------------------- - -Voir le [chapitre sur le routage |routing#Modules]. - - -Cartographie .[#toc-mapping] ----------------------------- - -Définit les règles par lesquelles le nom de la classe est dérivé du nom du présentateur. On les inscrit dans la [configuration] sous la clé `application › mapping`. - -Commençons par un exemple qui n'utilise pas de modules. Nous voulons simplement que les classes du présentateur aient l'espace de nom `App\Presenters`. Cela signifie qu'un présentateur tel que `Home` doit correspondre à la classe `App\Presenters\HomePresenter`. Ceci peut être réalisé par la configuration suivante : - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -Le nom du présentateur est remplacé par l'astérisque dans le masque de classe et le résultat est le nom de la classe. Facile ! - -Si nous divisons les présentateurs en modules, nous pouvons avoir notre propre mappage pour chaque module : - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Maintenant, le présentateur `Front:Home` correspond à la classe `App\Modules\Front\Presenters\HomePresenter` et le présentateur `Admin:Dashboard` à la classe `App\Modules\Admin\Presenters\DashboardPresenter`. - -Il est plus pratique de créer une règle générale (étoile) pour remplacer les deux premières. L'astérisque supplémentaire sera ajouté au masque de classe uniquement pour le module : - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Mais qu'en est-il si nous utilisons des modules imbriqués et que nous avons un présentateur `Admin:User:Edit`? Dans ce cas, le segment avec un astérisque représentant le module pour chaque niveau est simplement répété et le résultat est la classe `App\Modules\Admin\User\Presenters\EditPresenter`. - -Une notation alternative consiste à utiliser un tableau composé de trois segments au lieu d'une chaîne de caractères. Cette notation est équivalente à la précédente : - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -La valeur par défaut est `*: *Module\*Presenter`. diff --git a/application/fr/presenters.texy b/application/fr/presenters.texy index b6a772400e..538f20ab44 100644 --- a/application/fr/presenters.texy +++ b/application/fr/presenters.texy @@ -60,7 +60,7 @@ Similaire à la méthode `render()`. Alors que `render()` a pour but Il est important que `action()` soit appelé avant `render()`afin qu'à l'intérieur de celui-ci, nous puissions éventuellement modifier le cours suivant du cycle de vie, c'est-à-dire changer le modèle qui sera rendu et également la méthode `render()` qui sera appelée, en utilisant `setView('otherView')`. -Les paramètres de la requête sont transmis à la méthode. Il est possible et recommandé de spécifier des types pour les paramètres, par exemple `actionShow(int $id, string $slug = null)` - si le paramètre `id` est manquant ou s'il ne s'agit pas d'un nombre entier, le présentateur renvoie l'[erreur 404 |#Error 404 etc.] et met fin à l'opération. +Les paramètres de la requête sont transmis à la méthode. Il est possible et recommandé de spécifier des types pour les paramètres, par exemple `actionShow(int $id, ?string $slug = null)` - si le paramètre `id` est manquant ou s'il ne s'agit pas d'un nombre entier, le présentateur renvoie l'[erreur 404 |#Error 404 etc.] et met fin à l'opération. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ Dans le modèle, ces messages sont disponibles dans la variable `$flashes` en ta Erreur 404 etc. .[#toc-error-404-etc] ===================================== -Lorsque nous ne pouvons pas répondre à la demande, par exemple parce que l'article que nous voulons afficher n'existe pas dans la base de données, nous envoyons l'erreur 404 en utilisant la méthode `error(string $message = null, int $httpCode = 404)`, qui représente l'erreur HTTP 404 : +Lorsque nous ne pouvons pas répondre à la demande, par exemple parce que l'article que nous voulons afficher n'existe pas dans la base de données, nous envoyons l'erreur 404 en utilisant la méthode `error(?string $message = null, int $httpCode = 404)`, qui représente l'erreur HTTP 404 : ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Paramètres de la demande .[#toc-request-parameters] +=================================================== + +Le présentateur, ainsi que chaque composant, obtient ses paramètres à partir de la demande HTTP. Leurs valeurs peuvent être récupérées à l'aide de la méthode `getParameter($name)` ou `getParameters()`. Les valeurs sont des chaînes ou des tableaux de chaînes, essentiellement des données brutes obtenues directement à partir de l'URL. + +Pour plus de commodité, nous recommandons de rendre les paramètres accessibles par le biais de propriétés. Il suffit de les annoter avec l'attribut `#[Parameter]` attribut : + +```php +use Nette\Application\Attributes\Parameter; // cette ligne est importante + +class HomePresenter extends Nette\Application\UI\Presenter +{ + #[Parameter] + public string $theme; // doit être publique +} +``` + +Pour les propriétés, nous suggérons de spécifier le type de données (par exemple, `string`). Nette va alors automatiquement calculer la valeur en fonction de ce type de données. Les valeurs des paramètres peuvent également être [validées |#Validation of Parameters]. + +Lors de la création d'un lien, vous pouvez directement définir la valeur des paramètres : + +```latte +click +``` + + Paramètres persistants .[#toc-persistent-parameters] ==================================================== @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Si `$this->lang` a une valeur telle que `'en'`, les liens créés à l'aide de `link()` ou `n:href` contiendront également le paramètre `lang=en`. Et lorsque le lien sera cliqué, il s'agira à nouveau de `$this->lang = 'en'`. -Pour les propriétés, nous vous recommandons d'indiquer le type de données (par exemple `string`) et vous pouvez également inclure une valeur par défaut. Les valeurs des paramètres peuvent être [validées |#Validation of Persistent Parameters]. +Pour les propriétés, nous vous recommandons d'indiquer le type de données (par exemple `string`) et vous pouvez également inclure une valeur par défaut. Les valeurs des paramètres peuvent être [validées |#Validation of Parameters]. Les paramètres persistants sont transmis par défaut entre toutes les actions d'un présentateur donné. Pour les transmettre entre plusieurs présentateurs, vous devez les définir : @@ -307,18 +333,12 @@ Pour aller plus loin .[#toc-going-deeper] Ce que nous avons montré jusqu'à présent dans ce chapitre suffira probablement. Les lignes suivantes sont destinées à ceux qui s'intéressent aux présentateurs en profondeur et veulent tout savoir. -Exigences et paramètres .[#toc-requirement-and-parameters] +Validation des paramètres .[#toc-validation-of-parameters] ---------------------------------------------------------- -La requête traitée par le présentateur est l'objet [api:Nette\Application\Request] et est renvoyée par la méthode du présentateur `getRequest()`. Elle comprend un tableau de paramètres et chacun d'entre eux appartient soit à l'un des composants, soit directement au présentateur (qui est également un composant, bien qu'il soit spécial). Nette redistribue donc les paramètres et les transmet entre les différents composants (et le présentateur) en appelant la méthode `loadState(array $params)`. Les paramètres peuvent être obtenus par la méthode `getParameters(): array`, individuellement en utilisant `getParameter($name)`. Les valeurs des paramètres sont des chaînes ou des tableaux de chaînes, il s'agit essentiellement de données brutes obtenues directement à partir d'une URL. +Les valeurs des [paramètres de requête |#request parameters] et des [paramètres persistants |#persistent parameters] reçus des URL sont écrites dans les propriétés par la méthode `loadState()`. Elle vérifie également si le type de données spécifié dans la propriété correspond, sinon elle répondra par une erreur 404 et la page ne sera pas affichée. - -Validation des paramètres persistants .[#toc-validation-of-persistent-parameters] ---------------------------------------------------------------------------------- - -Les valeurs des [paramètres persistants |#persistent parameters] reçus des URL sont écrites dans les propriétés par la méthode `loadState()`. Elle vérifie également si le type de données spécifié dans la propriété correspond, sinon elle répondra par une erreur 404 et la page ne sera pas affichée. - -Ne faites jamais aveuglément confiance aux paramètres persistants, car ils peuvent facilement être remplacés par l'utilisateur dans l'URL. Par exemple, voici comment nous vérifions si `$this->lang` fait partie des langues prises en charge. Une bonne façon de le faire est de surcharger la méthode `loadState()` mentionnée ci-dessus : +Ne faites jamais aveuglément confiance aux paramètres, car ils peuvent facilement être remplacés par l'utilisateur dans l'URL. Par exemple, voici comment nous vérifions si `$this->lang` fait partie des langues prises en charge. Une bonne façon de le faire est de surcharger la méthode `loadState()` mentionnée ci-dessus : ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,7 +361,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Sauvegarde et restauration de la demande .[#toc-save-and-restore-the-request] ----------------------------------------------------------------------------- -Vous pouvez enregistrer la demande en cours dans une session ou la restaurer à partir de la session et laisser le présentateur l'exécuter à nouveau. Ceci est utile, par exemple, lorsqu'un utilisateur remplit un formulaire et que sa connexion expire. Afin de ne pas perdre de données, avant de rediriger l'utilisateur vers la page de connexion, nous sauvegardons la demande actuelle dans la session à l'aide de `$reqId = $this->storeRequest()`, qui renvoie un identifiant sous la forme d'une chaîne courte et le transmet comme paramètre au présentateur de connexion. +La demande traitée par le présentateur est un objet [api:Nette\Application\Request] et est renvoyée par la méthode du présentateur `getRequest()`. + +Vous pouvez enregistrer la requête en cours dans une session ou la restaurer à partir de la session et laisser le présentateur l'exécuter à nouveau. Ceci est utile, par exemple, lorsqu'un utilisateur remplit un formulaire et que son login expire. Afin de ne pas perdre de données, avant de rediriger l'utilisateur vers la page de connexion, nous sauvegardons la demande en cours dans la session à l'aide de la méthode `$reqId = $this->storeRequest()`, qui renvoie un identifiant sous la forme d'une chaîne courte et le transmet en tant que paramètre au présentateur de connexion. Après l'ouverture de session, nous appelons la méthode `$this->restoreRequest($reqId)`, qui récupère la demande de la session et la lui transmet. La méthode vérifie que la requête a été créée par le même utilisateur que celui qui est maintenant connecté. Si un autre utilisateur se connecte ou si la clé n'est pas valide, elle ne fait rien et le programme continue. @@ -362,7 +384,7 @@ La redirection ne se produit pas avec une demande AJAX ou POST, car elle entraî Vous pouvez également invoquer la canonisation manuellement à l'aide de la méthode `canonicalize()`, qui, comme la méthode `link()`, reçoit le présentateur, les actions et les paramètres comme arguments. Elle crée un lien et le compare à l'URL actuelle. Si elle est différente, elle redirige vers le lien généré. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // redirige si $slug est différent de $realSlug @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Restriction d'accès à l'aide de `#[Requires]` .[#toc-access-restriction-using-requires]{data-version:3.2.2} +----------------------------------------------------------------------------------------------------------- + +L'attribut `#[Requires]` fournit des options avancées pour restreindre l'accès aux présentateurs et à leurs méthodes. Il peut être utilisé pour spécifier des méthodes HTTP, exiger des requêtes AJAX, limiter l'accès à la même origine et restreindre l'accès à la transmission uniquement. L'attribut peut être appliqué aux classes de présentateurs ainsi qu'aux méthodes individuelles telles que `action()`, `render()`, `handle()`, et `createComponent()`. + +Vous pouvez spécifier ces restrictions : +- sur les méthodes HTTP : `#[Requires(methods: ['GET', 'POST'])]` +- nécessitant une requête AJAX : `#[Requires(ajax: true)]` +- accès uniquement à partir de la même origine : `#[Requires(sameOrigin: true)]` +- accès uniquement par le biais d'une redirection : `#[Requires(forward: true)]` +- restrictions sur des actions spécifiques : `#[Requires(actions: 'default')]` + +Pour plus de détails, voir [Comment utiliser l'attribut Requires |best-practices:attribute-requires]. + + +Vérification de la méthode HTTP .[#toc-http-method-check] +--------------------------------------------------------- + +Dans Nette, les présentateurs vérifient automatiquement la méthode HTTP de chaque requête entrante, principalement pour des raisons de sécurité. Par défaut, les méthodes `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` sont autorisées. + +Si vous souhaitez activer d'autres méthodes telles que `OPTIONS`, vous pouvez utiliser l'attribut `#[Requires]` (à partir de Nette Application v3.2) : + +```php +#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] +class MyPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Dans la version 3.1, la vérification est effectuée dans `checkHttpMethod()`, qui vérifie si la méthode spécifiée dans la requête est incluse dans le tableau `$presenter->allowedMethods`. Ajouter une méthode comme celle-ci : + +```php +class MyPresenter extends Nette\Application\UI\Presenter +{ + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } +} +``` + +Il est essentiel de souligner que si vous autorisez la méthode `OPTIONS`, vous devez également la gérer correctement dans votre présentateur. Cette méthode est souvent utilisée comme une requête dite "preflight", que les navigateurs envoient automatiquement avant la requête réelle lorsqu'il est nécessaire de déterminer si la requête est autorisée du point de vue de la politique CORS (Cross-Origin Resource Sharing). Si vous autorisez cette méthode mais ne mettez pas en œuvre une réponse appropriée, cela peut entraîner des incohérences et des problèmes de sécurité potentiels. + + Autres lectures .[#toc-further-reading] ======================================= diff --git a/application/fr/routing.texy b/application/fr/routing.texy index 9fe5d5e4a6..85f8f9473d 100644 --- a/application/fr/routing.texy +++ b/application/fr/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Ou nous pouvons utiliser cette forme, remarquez la réécriture de l'expression régulière de validation : +Pour une spécification plus détaillée, une forme encore plus étendue peut être utilisée, où en plus des valeurs par défaut, d'autres propriétés de paramètres peuvent être définies, telles qu'une expression régulière de validation (voir le paramètre `id` ) : ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Ces formats plus bavards sont utiles pour ajouter d'autres métadonnées. +Il est important de noter que si les paramètres définis dans le tableau ne sont pas inclus dans le masque de chemin, leurs valeurs ne peuvent pas être modifiées, même en utilisant des paramètres de requête spécifiés après un point d'interrogation dans l'URL. Filtres et traductions .[#toc-filters-and-translations] @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Modules .[#toc-modules] ----------------------- -Si nous avons plusieurs routes qui appartiennent à un seul [module |modules], nous pouvons utiliser `withModule()` pour les regrouper : +Si plusieurs itinéraires appartiennent à un même [module |directory-structure#Presenters and Templates], nous pouvons utiliser `withModule()` pour les regrouper : ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Intégration .[#toc-integration] =============================== -Afin de connecter notre routeur à l'application, nous devons en informer le conteneur DI. Le moyen le plus simple est de préparer la fabrique qui construira l'objet routeur et de dire à la configuration du conteneur de l'utiliser. Disons que nous écrivons une méthode dans ce but `App\Router\RouterFactory::createRouter()`: +Afin de connecter notre routeur à l'application, nous devons en informer le conteneur DI. Le moyen le plus simple est de préparer la fabrique qui construira l'objet routeur et de dire à la configuration du conteneur de l'utiliser. Disons que nous écrivons une méthode dans ce but `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ Puis nous écrivons dans la [configuration |dependency-injection:services]: ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Toutes les dépendances, telles qu'une connexion à une base de données, etc., sont transmises à la méthode factory en tant que paramètres en utilisant le [câblage automatique |dependency-injection:autowiring]: @@ -663,7 +663,7 @@ Par utilisation séparée, nous entendons l'utilisation des capacités du routeu Donc encore une fois nous allons créer une méthode qui va construire un routeur, par exemple : ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Ou bien nous créerons directement des objets : ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/fr/templates.texy b/application/fr/templates.texy index 8ecd733495..1867301258 100644 --- a/application/fr/templates.texy +++ b/application/fr/templates.texy @@ -34,35 +34,81 @@ Et ceci pourrait être le modèle d'action : Il définit le bloc `content`, qui est inséré à la place de `{include content}` dans la mise en page, et redéfinit également le bloc `title`, qui écrase `{block title}` dans la mise en page. Essayez d'imaginer le résultat. -Recherche de modèles .[#toc-search-for-templates] -------------------------------------------------- +Recherche de modèles .[#toc-template-lookup] +-------------------------------------------- -Le chemin vers les modèles est déduit selon une logique simple. Il essaie de voir si l'un de ces fichiers modèles existe par rapport au répertoire où se trouve la classe du présentateur, où `` est le nom du présentateur actuel et `` est le nom de l'action en cours : +Dans les présentateurs, vous n'avez pas besoin de spécifier quel modèle doit être rendu ; le cadre détermine automatiquement le chemin, ce qui facilite le codage. -- `templates//.latte` -- `templates/..latte` +Si vous utilisez une structure de répertoires dans laquelle chaque présentateur a son propre répertoire, placez simplement le modèle dans ce répertoire sous le nom de l'action (c'est-à-dire de la vue). Par exemple, pour l'action `default`, utilisez le modèle `default.latte`: -Si le modèle n'est pas trouvé, il essaiera de chercher dans le répertoire `templates` au niveau supérieur, c'est-à-dire au même niveau que le répertoire contenant la classe du présentateur. +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -Si le modèle n'y est pas trouvé non plus, la réponse est une [erreur 404 |presenters#Error 404 etc.]. +Si vous utilisez une structure dans laquelle les présentateurs sont regroupés dans un répertoire et les modèles dans un dossier `templates`, enregistrez-les dans un fichier `..latte` soit dans un fichier `/.latte`: -Vous pouvez également changer la vue en utilisant `$this->setView('otherView')`. Ou, au lieu de chercher, spécifiez directement le nom du fichier de modèle en utilisant `$this->template->setFile('/path/to/template.latte')`. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- + +Le répertoire `templates` peut également être placé un niveau plus haut, au même niveau que le répertoire des classes de présentateurs. + +Si le modèle n'est pas trouvé, le présentateur répond par l'[erreur 404 - page non trouvée |presenters#Error 404 etc]. + +Vous pouvez changer la vue en utilisant `$this->setView('anotherView')`. Il est également possible de spécifier directement le fichier de modèle avec `$this->template->setFile('/path/to/template.latte')`. .[note] -Vous pouvez modifier les chemins dans lesquels les modèles sont recherchés en remplaçant la méthode [formatTemplateFiles |api:Nette\Application\UI\Presenter::formatTemplateFiles()], qui renvoie un tableau de chemins de fichiers possibles. +Les fichiers dans lesquels les modèles sont recherchés peuvent être modifiés en remplaçant la méthode [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], qui renvoie un tableau de noms de fichiers possibles. + + +Recherche de modèles de mise en page .[#toc-layout-template-lookup] +------------------------------------------------------------------- + +Nette recherche également automatiquement le fichier de mise en page. + +Si vous utilisez une structure de répertoires dans laquelle chaque présentateur a son propre répertoire, placez le modèle soit dans le dossier du présentateur, s'il lui est propre, soit à un niveau supérieur s'il est commun à plusieurs présentateurs : + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +Si vous utilisez une structure dans laquelle les présentateurs sont regroupés dans un répertoire et les modèles dans un dossier `templates`, la mise en page sera attendue aux endroits suivants : -Le modèle est attendu dans les fichiers suivants : +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` mise en page commune à plusieurs présentateurs +Si le présentateur se trouve dans un module, il cherchera également plus haut dans l'arborescence du répertoire en fonction de l'imbrication du module. -`` est le nom du présentateur actuel et `` est le nom de la mise en page, qui est par défaut `'layout'`. Le nom peut être modifié avec `$this->setLayout('otherLayout')`, de sorte que les fichiers `@otherLayout.latte` seront essayés. +Le nom de la présentation peut être modifié à l'aide de `$this->setLayout('layoutAdmin')` et sera alors attendu dans le fichier `@layoutAdmin.latte`. Vous pouvez également spécifier directement le fichier de modèle de présentation en utilisant `$this->setLayout('/path/to/template.latte')`. -Vous pouvez également spécifier directement le nom du fichier du modèle de présentation en utilisant `$this->setLayout('/path/to/template.latte')`. L'utilisation de `$this->setLayout(false)` désactivera la recherche de la mise en page. +L'utilisation de `$this->setLayout(false)` ou de la balise `{layout none}` à l'intérieur du modèle désactive la recherche de modèle. .[note] -Vous pouvez modifier les chemins dans lesquels les modèles sont recherchés en remplaçant la méthode [formatLayoutTemplateFiles |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], qui renvoie un tableau de chemins de fichiers possibles. +Les fichiers dans lesquels les modèles de présentation sont recherchés peuvent être modifiés en remplaçant la méthode [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], qui renvoie un tableau de noms de fichiers possibles. Variables dans le modèle .[#toc-variables-in-the-template] @@ -104,7 +150,7 @@ L'annotation `@property-read` est pour les IDE et l'analyse statique, elle fera Vous pouvez aussi vous offrir le luxe de chuchoter dans les templates, il suffit d'installer le plugin Latte dans PhpStorm et de spécifier le nom de la classe au début du template, voir l'article "Latte : how to type system":https://blog.nette.org/fr/latte-comment-utiliser-le-systeme-de-type: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void Latte version 3 propose un moyen plus avancé en créant une [extension |latte:creating-extension] pour chaque projet web. Voici un exemple approximatif d'une telle classe : ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ Nous l'enregistrons en utilisant la [configuration |configuration#Latte]: ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ Alternativement, le traducteur peut être défini à l'aide de la [configuration ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` Le traducteur peut alors être utilisé, par exemple, comme un filtre `|translate`, avec des paramètres supplémentaires transmis à la méthode `translate()` (voir `foo, bar`) : diff --git a/application/hu/@home.texy b/application/hu/@home.texy index 49f809ce72..69024f5492 100644 --- a/application/hu/@home.texy +++ b/application/hu/@home.texy @@ -1,36 +1,85 @@ -Nette Application -***************** +Nette alkalmazás +**************** .[perex] -A `nette/application` csomag az interaktív webes alkalmazások készítésének alapja. +A Nette Application a Nette keretrendszer magja, amely hatékony eszközöket kínál a modern webes alkalmazások létrehozásához. Számos kivételes funkciót kínál, amelyek jelentősen leegyszerűsítik a fejlesztést és javítják a kód biztonságát és karbantarthatóságát. -- [Hogyan működnek az alkalmazások? |how-it-works] -- [Bootstrap |Bootstrap] -- [Bemutatók |Presenters] -- [Sablonok |Templates] -- [Modulok |Modules] -- [Útválasztás |Routing] -- [URL linkek létrehozása |creating-links] -- [Interaktív komponensek |components] -- [AJAX és snippetek |ajax] -- [Multiplikátor |multiplier] -- [Konfiguráció |Configuration] +Telepítés .[#toc-installation] +------------------------------ -Telepítés ---------- - -Töltse le és telepítse a csomagot a [Composer |best-practices:composer] segítségével: +Töltse le és telepítse a könyvtárat a [Composer |best-practices:composer] segítségével: ```shell composer require nette/application ``` + +Miért válassza a Nette alkalmazást? .[#toc-why-choose-nette-application] +------------------------------------------------------------------------ + +A Nette mindig is úttörő volt a webes technológiák terén. + +**Bidirekcionális útválasztó:** A Nette fejlett útválasztó rendszerrel rendelkezik, amely egyedülálló a kétirányúságában - nem csak az URL-eket fordítja le az alkalmazás műveleteire, hanem fordítva is képes URL-eket generálni. Ez azt jelenti, hogy: +- A teljes alkalmazás URL-struktúráját bármikor módosíthatja a sablonfájlok módosítása nélkül. +- Az URL-ek automatikusan kanonikalizálódnak, javítva a SEO-t +- Az útválasztás egy helyen kerül meghatározásra, nem pedig szétszórva a megjegyzésekben. + +**Komponensek és jelzések:** A beépített komponensrendszer, amelyet a Delphi és a React.js inspirált, egyedülálló a PHP keretrendszerek között: +- Lehetővé teszi újrafelhasználható UI elemek létrehozását +- Támogatja a hierarchikus komponenskompozíciót +- Elegáns AJAX kéréskezelést kínál jelek segítségével +- Kész komponensek gazdag könyvtára a [Componette](https://componette.org)oldalon. + +**AJAX és Snippets:** A Nette az AJAX-szel való munka forradalmi módját vezette be 2009-ben, még az olyan megoldások előtt, mint a Hotwire for Ruby on Rails vagy a Symfony UX Turbo: +- A snippetek lehetővé teszik az oldal csak egyes részeinek frissítését JavaScript írása nélkül. +- Automatikus integráció a komponensrendszerrel +- Az oldalrészek intelligens érvénytelenítése +- Minimális adatátvitel + +**Intuitív [Latte |latte:] sablonok:** A legbiztonságosabb sablonkészítő rendszer PHP számára, fejlett funkciókkal: +- Automatikus XSS-védelem kontextusérzékeny eszkalálással. +- Bővíthető egyéni szűrőkkel, függvényekkel és címkékkel +- Sablon öröklés és snippetek AJAX-hoz +- Kiváló PHP 8.x támogatás a típusrendszerrel + +**Dependency Injection:** A Nette teljes mértékben kihasználja a Dependency Injectiont: +- Automatikus függőségi átadás (autowiring) +- Konfiguráció egyértelmű NEON formátumban +- Komponens gyárak támogatása + + +Főbb előnyök .[#toc-main-benefits] +---------------------------------- + +- **Biztonság**: XSS, CSRF stb. ellen. +- **Produktivitás**: Kevesebb írás, több funkció az intelligens tervezésnek köszönhetően +- **Hibakeresés**: [Tracy debugger |tracy:] a routing panellel +- **Teljesítmény**: Intelligens gyorsítótárazási rendszer, a komponensek lusta betöltése. +- **Flexibilitás**: Könnyű URL módosítás az alkalmazás befejezése után is +- **Komponensek**: Újrafelhasználható UI-elemek egyedülálló rendszere +- **Modern**: A PHP 8.4+ és a típusrendszer teljes támogatása + + +Első lépések .[#toc-getting-started] +------------------------------------ + +1. [Az alkalmazások megértése |how-it-works] - Az alapvető architektúra megértése +2. [Bemutatók |presenters] - Munka a bemutatókkal és a műveletekkel +3. [Sablonok |templates] - Sablonok létrehozása a Latte-ban +4. [Útválasztás |routing] - URL-konfiguráció +5. [Interaktív komponensek |components] - A komponensrendszer használata + + +PHP kompatibilitás .[#toc-php-compatibility] +-------------------------------------------- + | verzió | kompatibilis a PHP-vel |-----------|------------------- -| Nette alkalmazás 4.0 | PHP 8.0 - 8.2 -| Nette alkalmazás 3.1 | PHP 7.2 - 8.2 +| Nette alkalmazás 4.0 | PHP 8.1 - 8.4 +| Nette alkalmazás 3.2 | PHP 8.1 - 8.4 +| Nette alkalmazás 3.1 | PHP 7.2 - 8.3 | Nette alkalmazás 3.0 | PHP 7.1 - 8.0 -| Nette Application 2.4 | PHP 5.6 - 8.0 +| Nette alkalmazás 2.4 | PHP 5.6 - 8.0 -A legújabb javítási verziókra vonatkozik. +A legújabb javítási verziókra érvényes. \ No newline at end of file diff --git a/application/hu/@left-menu.texy b/application/hu/@left-menu.texy index ab68654c37..397c312529 100644 --- a/application/hu/@left-menu.texy +++ b/application/hu/@left-menu.texy @@ -4,7 +4,7 @@ Nette alkalmazás - [Bootstrap |Bootstrap] - [Bemutatók |Presenters] - [Sablonok |Templates] -- [Modulok |Modules] +- [Könyvtárszerkezet |directory-structure] - [Útválasztás |Routing] - [URL linkek létrehozása |creating-links] - [Interaktív komponensek |components] diff --git a/application/hu/ajax.texy b/application/hu/ajax.texy index fb0a6e08b8..b2a6f90cc9 100644 --- a/application/hu/ajax.texy +++ b/application/hu/ajax.texy @@ -3,10 +3,10 @@ AJAX & Snippetek
    -A modern webes alkalmazások manapság félig a szerveren, félig a böngészőben futnak. Az AJAX létfontosságú egyesítő tényező. Milyen támogatást nyújt a Nette Framework? -- sablonrészletek küldése (ún. *snippetek*) -- változók átadása PHP és JavaScript között -- AJAX-alkalmazások hibakeresése +A modern webes alkalmazások korában, ahol a funkciók gyakran a szerver és a böngésző között helyezkednek el, az AJAX elengedhetetlen összekötő elem. Milyen lehetőségeket kínál a Nette Framework ezen a területen? +- A sablon részeinek, az ún. snippeteknek a küldése. +- változók átadása a PHP és a JavaScript között +- AJAX-kérések hibakeresésére szolgáló eszközök
    @@ -14,29 +14,32 @@ A modern webes alkalmazások manapság félig a szerveren, félig a böngészőb AJAX kérés .[#toc-ajax-request] =============================== -Az AJAX-kérés nem különbözik a klasszikus kéréstől - a bemutatót egy adott nézettel és paraméterekkel hívják meg. Az is a prezenteren múlik, hogyan válaszol rá: használhat saját rutint, amely egy HTML kódrészletet (HTML snippet), egy XML-dokumentumot, egy JSON-objektumot vagy JavaScript-kódot ad vissza. +Az AJAX-kérés alapvetően nem különbözik a klasszikus HTTP-kéréstől. Egy bemutatót hívunk meg bizonyos paraméterekkel. A bemutatótól függ, hogyan válaszol a kérésre - adhat vissza adatokat JSON formátumban, küldhet egy HTML kódrészt, egy XML-dokumentumot stb. -Kiszolgálói oldalon az AJAX-kérés a [HTTP-kérést kapszulázó |http:request] szolgáltatási módszerrel detektálható `$httpRequest->isAjax()` (a HTTP fejléc alapján detektál `X-Requested-With`). A prezenteren belül a `$this->isAjax()` metódus formájában egy rövidítés áll rendelkezésre. +A böngésző oldalán AJAX-kérést kezdeményezünk a `fetch()` funkcióval: -Létezik egy `payload` nevű előfeldolgozott objektum, amely arra szolgál, hogy az adatokat JSON-ban küldje el a böngészőnek. - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // a válasz feldolgozása +}); ``` -A JSON kimenet teljes ellenőrzéséhez használja a `sendJson` metódust a prezenterben. Ez azonnal befejezi a prezentert, és sablon nélkül is boldogulsz: +A szerveroldalon az AJAX-kérést a [HTTP-kérést kapszulázó |http:request] szolgáltatás `$httpRequest->isAjax()` módszere ismeri fel. Ez a `X-Requested-With` HTTP fejlécet használja, ezért elengedhetetlen a küldése. A prezenteren belül a `$this->isAjax()` metódust használhatja. + +Ha JSON formátumban szeretne adatokat küldeni, használja a [`sendJson()` |presenters#Sending a response] módszert. A metódus a prezenter tevékenységét is befejezi. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Ha HTML-t szeretnénk küldeni, akkor vagy beállíthatunk egy speciális sablont az AJAX-kérésekhez: +Ha azt tervezi, hogy egy speciális, AJAX-hoz tervezett sablonnal válaszol, akkor azt a következőképpen teheti meg: ```php public function handleClick($param): void @@ -44,27 +47,43 @@ public function handleClick($param): void if ($this->isAjax()) { $this->template->setFile('path/to/ajax.latte'); } - // ... + //... } ``` +Snippets .[#toc-snippets] +========================= + +A Nette által kínált leghatékonyabb eszköz a szerver és az ügyfél összekapcsolására a snippetek. Segítségükkel egy közönséges alkalmazásból minimális erőfeszítéssel és néhány sornyi kóddal AJAX-alkalmazássá alakítható. A Fifteen példája bemutatja, hogyan működik mindez, és a kódja megtalálható a [GitHubon |https://github.com/nette-examples/fifteen]. + +A snippetek, vagyis a kivágások lehetővé teszik, hogy az oldalnak csak egyes részeit frissítsük, ahelyett, hogy az egész oldalt újratöltenénk. Ez gyorsabb és hatékonyabb, és kényelmesebb felhasználói élményt is nyújt. A snippetek a Ruby on Rails Hotwire vagy a Symfony UX Turbo programjára emlékeztethetnek. Érdekes módon a Nette 14 évvel korábban vezette be a snippeteket. + +Hogyan működnek a snippetek? Az oldal első betöltésekor (egy nem-AJAX kérés) a teljes oldal betöltődik, beleértve az összes snippetet is. Amikor a felhasználó interakcióba lép az oldallal (pl. rákattint egy gombra, elküld egy űrlapot stb.), a teljes oldal betöltése helyett AJAX-kérés történik. A prezenterben lévő kód végzi el a műveletet, és dönti el, hogy mely snippetek igényelnek frissítést. A Nette rendereli ezeket a részleteket, és egy JSON tömb formájában elküldi őket. A böngészőben lévő kezelő kód ezután visszahelyezi a kapott részleteket az oldalba. Ezért csak a módosított snippetek kódja kerül átvitelre, ami a teljes oldaltartalom átviteléhez képest sávszélességet takarít meg és gyorsítja a betöltést. + + Naja .[#toc-naja] -================= +----------------- -A [Naja könyvtár |https://naja.js.org] az AJAX kérések kezelésére szolgál a böngésző oldalán. [Telepítsd |https://naja.js.org/#/guide/01-install-setup-naja] node.js csomagként (Webpack, Rollup, Vite, Parcel és más csomagokkal való használathoz): +A snippetek böngészőoldali kezeléséhez a [Naja könyvtárat |https://naja.js.org] használjuk. [Telepítse |https://naja.js.org/#/guide/01-install-setup-naja] node.js csomagként (olyan alkalmazásokkal használható, mint a Webpack, Rollup, Vite, Parcel és mások): ```shell npm install naja ``` -...vagy illessze be közvetlenül az oldal sablonjába: +... vagy illessze be közvetlenül az oldal sablonjába: ```html ``` -Ahhoz, hogy AJAX-kérést hozzon létre egy hagyományos linkből (jel) vagy űrlapküldésből, egyszerűen jelölje meg az adott linket, űrlapot vagy gombot a `ajax` osztállyal: +Először [inicializálni |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] kell a könyvtárat: + +```js +naja.initialize(); +``` + +Ahhoz, hogy egy közönséges linket (jelet) vagy űrlapküldést AJAX-kérelemmé tegyen, egyszerűen jelölje meg az adott linket, űrlapot vagy gombot a `ajax` osztállyal: ```html Go @@ -74,64 +93,39 @@ Ahhoz, hogy AJAX-kérést hozzon létre egy hagyományos linkből (jel) vagy űr or +
    ``` -Snippets .[#toc-snippets] -========================= - -A beépített AJAX-támogatásnak van egy sokkal hatékonyabb eszköze - a snippetek. Használatuk lehetővé teszi, hogy egy hagyományos alkalmazásból AJAX-alkalmazássá váljon mindössze néhány sornyi kóddal. Hogy mindez hogyan működik, azt a Fifteen példa mutatja be, amelynek kódja a buildben vagy a [GitHubon |https://github.com/nette-examples/fifteen] is elérhető. - -A snippetek úgy működnek, hogy a kezdeti (azaz nem AJAX) kérés során a teljes oldal átkerül, majd minden AJAX [alkérésnél |components#signal] (ugyanazon bemutató azonos nézetének kérése) csak a módosított részek kódja kerül át a már említett `payload` tárolóba. +Snippetek újrarajzolása .[#toc-redrawing-snippets] +-------------------------------------------------- -A Snippetek a Ruby on Rails Hotwire vagy a Symfony UX Turbo-ra emlékeztethetnek, de Nette tizennégy évvel korábban találta ki őket. - - -A Snippetek érvénytelenítése .[#toc-invalidation-of-snippets] -============================================================= - -A [Control |components] osztály minden leszármazottja (ami egy Presenter is) képes megjegyezni, hogy egy kérés során történt-e olyan változás, ami miatt újra kell rendeznie. Van egy pár módszer ennek kezelésére: `redrawControl()` és `isControlInvalid()`. Egy példa: +A [Control |components] osztály minden objektuma (beleértve magát a Presentert is) nyilvántartást vezet arról, hogy történtek-e olyan változások, amelyek miatt újra kell rajzolni. Erre a célra a `redrawControl()` metódus szolgál. ```php public function handleLogin(string $user): void { - // Az objektumot újra kell renderelni, miután a felhasználó bejelentkezett. + // a bejelentkezés után újra kell rajzolni a vonatkozó részt $this->redrawControl(); - // ... + //... } ``` -A Nette azonban még finomabb felbontást kínál, mint az egész komponensek. A felsorolt módszerek opcionális paraméterként elfogadják egy úgynevezett "snippet" nevét. A "snippet" alapvetően a sablonod egy eleme, amelyet erre a célra egy Latte makróval jelöltél meg, erről később. Így lehetőség van arra, hogy egy komponenst arra kérjünk, hogy csak a sablonjának *részeit* rajzolja újra. Ha a teljes komponens érvénytelenítésre kerül, akkor az összes snippetje újrarendezésre kerül. Egy komponens akkor is "érvénytelen", ha bármelyik alkomponense érvénytelen. - -```php -$this->isControlInvalid(); // -> false -$this->redrawControl('header'); // érvényteleníti a 'header' nevű snippet-t. -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, legalább egy snippet érvénytelen. +A Nette lehetővé teszi annak finomabb ellenőrzését is, hogy mit kell átrajzolni. A fent említett módszer argumentumként elfogadhatja a snippet nevét. Így lehetséges a sablonrész szintjén érvényteleníteni (azaz: újrarajzolást kikényszeríteni). Ha a teljes komponens érvénytelenítése megtörténik, akkor annak minden egyes részlete is újrarajzolásra kerül: -$this->redrawControl(); // érvényteleníti az egész komponenst, minden egyes snippet-tel együtt. -$this->isControlInvalid('footer'); // -> true +```php +// érvényteleníti a "fejléc"-részletet. +$this->redrawControl('header'); ``` -A jelzést kapó komponens automatikusan újrarajzolásra kerül. - -A snippet-újrarajzolásnak köszönhetően pontosan tudjuk, hogy mely elemek mely részeit kell újrarajzolni. - - -Tag `{snippet} … {/snippet}` .{toc: Tag snippet} -================================================ -Az oldal megjelenítése nagyon hasonlóan zajlik, mint egy normál kérésnél: ugyanazok a sablonok töltődnek be stb. A lényeges rész azonban az, hogy a kimenetre nem szánt részek kimaradnak; a többi részhez egy azonosítót kell társítani, és egy JavaScript kezelő számára érthető formátumban kell elküldeni a felhasználónak. +Szemelvények a Latte-ban .[#toc-snippets-in-latte] +-------------------------------------------------- - -Szintaxis .[#toc-syntax] ------------------------- - -Ha a sablonban van egy vezérlőelem vagy egy snippet, akkor azt a `{snippet} ... {/snippet}` páros taggel kell becsomagolnunk - ez biztosítja, hogy a renderelt snippet "kivágásra" kerüljön, és elküldjük a böngészőnek. Ez is egy segítőbe fogja beburkolni. `
    ` tagbe (lehet másikat is használni). A következő példában egy `header` nevű snippet van definiálva. Ez akár egy komponens sablonját is jelentheti: +A snippetek használata a Latte-ban rendkívül egyszerű. Ha a sablon egy részét snippetként szeretné definiálni, egyszerűen csomagolja be a `{snippet}` és `{/snippet}` címkékbe: ```latte {snippet header} @@ -139,7 +133,9 @@ Ha a sablonban van egy vezérlőelem vagy egy snippet, akkor azt a `{snippet} .. {/snippet} ``` -A snippet egy más típusú snippet, mint a `
    ` vagy egy további HTML-attribútumokkal ellátott snippet az attribútumváltozat használatával érhető el: +A snippet létrehoz egy elemet `
    ` a HTML-oldalon egy speciálisan generált `id` címmel. A snippet újrarajzolásakor ennek az elemnek a tartalma frissül. Ezért az oldal kezdeti megjelenítésekor az összes snippetet is meg kell jeleníteni, még akkor is, ha azok kezdetben üresek lehetnek. + +A snippet létrehozható egy másik elemmel is, mint a `
    ` n:attribútummal: ```latte
    @@ -148,138 +144,106 @@ A snippet egy más típusú snippet, mint a `
    ` vagy egy további HTML-attri ``` -Dynamic Snippets .[#toc-dynamic-snippets] -========================================= +Snippet területek .[#toc-snippet-areas] +--------------------------------------- -A Nette-ben egy futásidejű paraméter alapján dinamikus névvel ellátott snippeteket is definiálhat. Ez leginkább olyan különböző listákhoz alkalmas, ahol csak egy sort kell megváltoztatnunk, de nem akarjuk vele együtt az egész listát is átvinni. Egy példa erre a következő lenne: +A snippet nevek kifejezések is lehetnek: ```latte -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Van egy statikus snippet, a `itemsContainer`, amely több dinamikus snippet tartalmaz: `item-0`, `item-1` és így tovább. + `item-0`, `item-1`, stb. Ha közvetlenül érvénytelenítenénk egy dinamikus snippetet (pl. `item-1`), akkor semmi sem rajzolódna újra. A snippetek ugyanis valódi részletként működnek, és csak maguk a snippetek kerülnek közvetlenül megjelenítésre. A sablonban azonban technikailag nincs `item-1` nevű snippet. Ez csak a snippet környező kódjának, jelen esetben a foreach ciklusnak a végrehajtásakor jelenik meg. Ezért a sablon azon részét, amelyet végre kell hajtani, a `{snippetArea}` címkével jelöljük: -Egy dinamikus snippet közvetlenül nem rajzolható újra (a `item-1` újrarajzolásának nincs hatása), a szülő snippetjét (ebben a példában `itemsContainer`) kell újrarajzolni. Ennek hatására a szülő snippet kódja végrehajtódik, de ezután csak az alszippetjei kerülnek elküldésre a böngészőnek. Ha csak az egyik alrészletet szeretné átküldeni, akkor a szülő részlet bemenetét úgy kell módosítania, hogy a többi alrészletet ne generálja. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -A fenti példában gondoskodnia kell arról, hogy egy AJAX-kérés esetén csak egy elem kerüljön a `$list` tömbhöz, ezért a `foreach` ciklus csak egy dinamikus snippetet fog kiírni. +És újrarajzoljuk mind az egyes snippetet, mind a teljes átfogó területet: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * This method returns data for the list. - * Usually this would just request the data from a model. - * For the purpose of this example, the data is hard-coded. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +Lényeges annak biztosítása is, hogy a `$items` tömb csak azokat az elemeket tartalmazza, amelyeket újra kell rajzolni. -Snippetek egy belefoglalt sablonban .[#toc-snippets-in-an-included-template] -============================================================================ - -Előfordulhat, hogy a snippet egy olyan sablonban van, amely egy másik sablonból van beépítve. Ebben az esetben a második sablonban a `snippetArea` makróval be kell csomagolnunk a beillesztési kódot, majd újra kell rajzolnunk mind a snippetArea-t, mind a tényleges snippetet. - -A `snippetArea` makró biztosítja, hogy a benne lévő kód végrehajtásra kerül, de csak a tényleges snippet kerül a böngészőhöz a bevont sablonban. +Ha a `{include}` tag segítségével egy másik sablont illesztünk be a fő sablonba, amely szeleteket tartalmaz, akkor a bevont sablont ismét be kell csomagolni a `snippetArea` címkébe, és a szeletet és a területet együttesen érvényteleníteni kell: ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* included.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -Dinamikus snippetekkel is kombinálható. - - -Hozzáadás és törlés .[#toc-adding-and-deleting] -=============================================== -Ha új elemet adsz hozzá a listához, és érvényteleníted a `itemsContainer` címet, az AJAX-kérés az új elemet is tartalmazó részleteket küldi vissza, de a javascript kezelő nem tudja megjeleníteni azt. Ennek oka, hogy nincs olyan HTML-elem, amely az újonnan létrehozott azonosítóval rendelkezik. +Snippetek az összetevőkben .[#toc-snippets-in-components] +--------------------------------------------------------- -Ebben az esetben a legegyszerűbb megoldás, ha az egész listát még egy snippetbe csomagoljuk, és az egészet érvénytelenítjük: + [Komponenseken |components] belül is létrehozhat snippeteket, amelyeket a Nette automatikusan átrajzol. Van azonban egy sajátos korlátozás: a snippetek újrarajzolásához paraméterek nélkül hívja meg a `render()` metódust. Így a paraméterek átadása a sablonban nem fog működni: ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Felhasználói adatok küldése .[#toc-sending-user-data] +----------------------------------------------------- + +A snippetek mellett bármilyen további adatot is küldhet az ügyfélnek. Egyszerűen írja őket a `payload` objektumba: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + //... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -Ugyanez vonatkozik egy elem törlésére is. Lehetséges lenne üres snippet küldése, de általában a listák lehetnek oldalszámozottak, és bonyolult lenne megvalósítani egy elem törlését és egy másik betöltését (amely korábban a oldalszámozott lista egy másik oldalán volt). - -Paraméterek küldése a komponensnek .[#toc-sending-parameters-to-component] -========================================================================== +Küldési paraméterek .[#toc-sending-parameters] +============================================== Amikor AJAX-kérésen keresztül paramétereket küldünk a komponensnek, legyenek azok jelparaméterek vagy állandó paraméterek, 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 -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -És kezelje a módszert s megfelelő paraméterekkel a komponensben. +A komponensben lévő megfelelő paraméterekkel rendelkező handle metódust: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/hu/bootstrap.texy b/application/hu/bootstrap.texy index 5d0a614439..3b307adc6a 100644 --- a/application/hu/bootstrap.texy +++ b/application/hu/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + private Configurator $configurator; + private string $rootDir; + + public function __construct() + { + $this->rootDir = dirname(__DIR__); + // A konfigurátor felelős az alkalmazási környezet és a szolgáltatások beállításáért. + $this->configurator = new Configurator; + // A Nette által generált ideiglenes fájlok (pl. lefordított sablonok) könyvtárának beállítása. + $this->configurator->setTempDirectory($this->rootDir . '/temp'); + } + + public function bootWebApplication(): Nette\DI\Container + { + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); + } + + private function initializeEnvironment(): void { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // A Nette intelligens, és a fejlesztői üzemmód automatikusan bekapcsol, + // vagy engedélyezheti egy adott IP-címre a következő sor megjegyzésének feloldásával: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Bekapcsolja a Tracy-t: a végső "svájci bicska" hibakeresési eszköz. + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: automatikusan feltölti az összes osztályt a megadott könyvtárban. + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Konfigurációs fájlok betöltése + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php .[#toc-index-php] =========================== -A webes alkalmazások esetében a kezdő fájl a `index.php`, amely a `www/` nyilvános könyvtárban található. Ez lehetővé teszi a `Bootstrap` osztály számára, hogy inicializálja a környezetet, és visszaadja a `$configurator`, amely létrehozza a DI konténert. Ezután megszerzi a `Application` szolgáltatást, amely a webalkalmazást futtatja: +A webes alkalmazások esetében az elsődleges fájl a `index.php`, amely a `www/`[nyilvános könyvtárban |directory-structure#public-directory-www] található. Ez a Bootstrap osztállyal inicializálja a környezetet, és létrehoz egy DI konténert. Ezután megkapja belőle a `Application` szolgáltatást, amely elindítja a webalkalmazást: ```php -// a környezet inicializálása + konfigurátor objektum kinyerése -$configurator = App\Bootstrap::boot(); -// DI konténer létrehozása -$container = $configurator->createContainer(); +$bootstrap = new App\Bootstrap; +// A környezet inicializálása + DI konténer létrehozása +$container = $bootstrap->bootWebApplication(); // A DI konténer létrehoz egy Nette\Application\Application objektumot. $application = $container->getByType(Nette\Application\Application::class); -// Nette alkalmazás indítása +// A Nette-alkalmazás elindítása és a bejövő kérések kezelése. $application->run(); ``` @@ -59,26 +84,42 @@ Mint látható, a [api:Nette\Bootstrap\Configurator] osztály, amelyet most rés Fejlesztői vs. termelési üzemmód .[#toc-development-vs-production-mode] ======================================================================= -A Nette két alapvető módot különböztet meg, amelyben egy kérés végrehajtásra kerül: fejlesztési és termelési mód. A fejlesztési mód a programozó maximális kényelmére összpontosít, a Tracy megjelenik, a gyorsítótár automatikusan frissül, ha a sablonok vagy a DI konténer konfigurációja változik, stb. A termelési mód a teljesítményre összpontosít, a Tracy csak a hibákat naplózza, a sablonok és egyéb fájlok módosításait nem ellenőrzi. +A Nette másképp viselkedik attól függően, hogy fejlesztési vagy termelési szerveren fut: + +🛠️ Fejlesztési mód: + - Megjeleníti a Tracy hibakereső sávot hasznos információkkal (pl. SQL-lekérdezések, végrehajtási idő, memóriahasználat). + - Hiba esetén részletes hibalevelet jelenít meg függvényhívás nyomvonalakkal és változótartalommal. + - Automatikusan frissíti a gyorsítótárat, amikor a Latte sablonok, konfigurációs fájlok stb. módosulnak. + + +🚀 Termelési mód: + - Nem jelenít meg semmilyen hibakeresési információt; minden hiba naplózásra kerül. + - Hiba esetén megjelenít egy `ErrorPresenter` vagy egy általános "Szerverhiba" oldalt. + - A gyorsítótár soha nem frissül automatikusan! + - A sebességre és a biztonságra optimalizált. -A mód kiválasztása automatikus felismeréssel történik, így általában nem szükséges kézzel konfigurálni vagy váltani semmit. A mód fejlesztési, ha az alkalmazás localhoston fut (azaz a `127.0.0.1` vagy a `::1` IP-címen ) és nincs proxy (azaz a HTTP fejléce). Ellenkező esetben termelési üzemmódban fut. + +Az üzemmódot automatikusan határozza meg, így a legtöbb esetben nincs szükség a manuális beállításra vagy váltásra: + +- Fejlesztési üzemmód: `127.0.0.1` vagy `::1`), kivéve, ha proxy van használatban (azaz a HTTP fejlécek alapján). +- Gyártási üzemmód: Mindenhol máshol aktív. Ha más esetekben, például egy adott IP-címről hozzáférő programozók számára szeretné engedélyezni a fejlesztési üzemmódot, akkor a `setDebugMode()` címet használhatja: ```php -$configurator->setDebugMode('23.75.345.200'); // egy vagy több IP-cím +$this->configurator->setDebugMode('23.75.345.200'); // egy vagy több IP-cím ``` Mindenképpen javasoljuk az IP-cím és a cookie kombinálását. A `nette-debug` cookie-ban tárolunk egy titkos tokent, pl. `secret1234`, és a fejlesztési mód az IP és a cookie ilyen kombinációjával rendelkező programozók számára aktiválódik. ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` A fejlesztői módot teljesen ki is kapcsolhatjuk, akár a localhost esetében is: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` A `true` érték keményen bekapcsolja a fejlesztői módot, ami soha nem történhet meg egy termelő szerveren. @@ -90,7 +131,7 @@ Hibakereső eszköz Tracy .[#toc-debugging-tool-tracy] Az egyszerű hibakeresés érdekében bekapcsoljuk a [Tracy |tracy:] nevű nagyszerű eszközt. Fejlesztői módban megjeleníti a hibákat, termelési módban pedig a megadott könyvtárba naplózza a hibákat: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ Ideiglenes fájlok .[#toc-temporary-files] A Nette a DI konténer, a RobotLoader, a sablonok stb. számára használja a gyorsítótárat. Ezért szükséges annak a könyvtárnak az elérési útvonalát beállítani, ahol a gyorsítótár tárolásra kerül: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` Linuxon vagy macOS-en állítsa be a `log/` és a `temp/` könyvtárak [írási engedélyeit |nette:troubleshooting#Setting directory permissions]. @@ -112,7 +153,7 @@ RobotLoader .[#toc-robotloader] Általában a [RobotLoader |robot-loader:] segítségével szeretnénk automatikusan betölteni az osztályokat, 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 annak összes alkönyvtárából: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ Időzóna .[#toc-timezone] A Configurator lehetővé teszi, hogy megadjon egy időzónát az alkalmazásához. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ Fejlesztői üzemmódban a konténer automatikusan frissül minden alkalommal, a A konfigurációs fájlok betöltése a `addConfig()` segítségével történik: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` A `addConfig()` metódus többször is meghívható több fájl hozzáadásához. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ Statikus paraméterek .[#toc-static-parameters] A konfigurációs fájlokban használt paramétereket a [`parameters` szakaszban |dependency-injection:configuration#parameters] lehet definiálni, és a `addStaticParameters()` metódus (amelynek alias neve `addParameters()`) is átadhatja (vagy felülírhatja). Fontos, hogy a különböző paraméterértékek további DI-konténerek, azaz további osztályok generálását okozzák. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ Dinamikus paraméterek .[#toc-dynamic-parameters] A konténerhez dinamikus paramétereket is hozzáadhatunk, ezek eltérő értékei a statikus paraméterekkel ellentétben nem okoznak új DI-konténerek generálását. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ A környezeti változókat könnyen elérhetővé tehetnénk dinamikus paraméterek segítségével. A konfigurációs fájlokban a `%env.variable%` címen keresztül érhetjük el őket. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ A konfigurációs fájlokban a következő statikus paramétereket használhatja - `%wwwDir%` a `index.php` beviteli fájlt tartalmazó könyvtár abszolút elérési útja. - `%tempDir%` az ideiglenes fájlok könyvtárának abszolút elérési útja. - `%vendorDir%` az abszolút elérési út a könyvtárak Composer általi telepítésének könyvtárához. +- `%rootDir%` a projekt gyökérkönyvtárának abszolút elérési útvonala. - `%debugMode%` jelzi, hogy az alkalmazás hibakeresési módban van-e. - `%consoleMode%` jelzi, hogy a kérés a parancssoron keresztül érkezett-e. @@ -225,7 +268,7 @@ services: Hozzunk létre egy új példányt, és illesszük be a bootstrapbe: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Különböző környezetek .[#toc-different-environments] ==================================================== -Nyugodtan testre szabhatja a `Bootstrap` osztályt, hogy megfeleljen az igényeinek. A `boot()` metódushoz paramétereket adhat a webes projektek megkülönböztetéséhez, vagy más metódusokat is hozzáadhat, például a `bootForTests()`, amely inicializálja a környezetet a unit tesztekhez, a `bootForCli()` a parancssorból hívott szkriptekhez, és így tovább. +Ne habozzon, ha a `Bootstrap` osztályt saját igényei szerint alakíthatja. A `bootWebApplication()` metódushoz paramétereket adhat hozzá a webes projektek megkülönböztetése érdekében. Alternatívaként más metódusokat is hozzáadhat, például a `bootTestEnvironment()` a környezet inicializálásához a unit tesztekhez, a `bootConsoleApplication()` a parancssorból hívott szkriptekhez stb. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container { - $configurator = self::boot(); Tester\Environment::setup(); // Nette Tester inicializálása - return $configurator; + $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 index 410cca3a9e..0e10d16126 100644 --- a/application/hu/components.texy +++ b/application/hu/components.texy @@ -230,6 +230,28 @@ A sablonban ezek az üzenetek a `$flashes` változóban állnak rendelkezésre, ``` +Átirányítás jelzést követően .[#toc-redirection-after-a-signal] +=============================================================== + +Egy komponensjel feldolgozása után gyakran következik az átirányítás. Ez a helyzet hasonló az űrlapokhoz - egy űrlap elküldése után mi is átirányítunk, hogy megakadályozzuk az adatok újbóli elküldését, amikor az oldal frissül a böngészőben. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Mivel a komponens egy újrafelhasználható elem, és általában nem szabad, hogy közvetlen függőségben álljon az egyes prezenterektől, a `redirect()` és a `link()` metódusok automatikusan komponensjelként értelmezik a paramétert: + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +Ha át kell irányítani egy másik prezenterre vagy műveletre, akkor ezt a prezenteren keresztül teheti meg: + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Állandó paraméterek .[#toc-persistent-parameters] ================================================= @@ -347,7 +369,7 @@ services: Végül ezt a gyárat fogjuk használni a prezenterünkben: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ A komponensek mélysége .[#toc-components-in-depth] A komponensek egy Nette alkalmazásban a webalkalmazás újrafelhasználható részei, amelyeket oldalakba ágyazunk be, ez a fejezet témája. Pontosan milyen képességekkel rendelkezik egy ilyen komponens? 1) egy sablonban megjeleníthető. -2) tudja, hogy egy [AJAX-kérés |ajax#invalidation] során melyik részét kell renderelni (snippet) +2) tudja, hogy egy AJAX-kérés során [melyik |ajax#snippets] részét kell megjelenítenie (snippetek). 3) képes az állapotát egy URL-ben tárolni (állandó paraméterek). 4) képes reagálni a felhasználói műveletekre (jelek) 5) hierarchikus struktúrát hoz létre (ahol a gyökér a bemutató) diff --git a/application/hu/configuration.texy b/application/hu/configuration.texy index fc96b2a039..0c068dd444 100644 --- a/application/hu/configuration.texy +++ b/application/hu/configuration.texy @@ -14,10 +14,14 @@ application: debugger: ... # (bool) alapértelmezett értéke true # error-presenter meghívásra kerül hiba esetén? - catchExceptions: ... # (bool) alapértelmezés szerint true termelési üzemmódban. + # csak fejlesztői módban van hatása + catchExceptions: ... # (bool) alapértelmezés szerint true # error-presenter neve - errorPresenter: Error # (string) alapértelmezett értéke 'Nette:Error'. + errorPresenter: Error # (string|array) alapértelmezett értéke 'Nette:Error'. + + # aliasokat definiál az előadókhoz és eseményekhez + aliases: ... # meghatározza a prezenter nevének egy osztályra való feloldására vonatkozó szabályokat. mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) alapértelmezett értéke false ``` -Mivel a hiba-bemutatókat alapértelmezés szerint nem hívja meg a program fejlesztési módban, és a hibákat a Tracy jeleníti meg, a `catchExceptions` érték `true` -re történő módosítása segít ellenőrizni, hogy a hiba-bemutatók helyesen működnek-e a fejlesztés során. +A `nette/application` 3.2-es verziójától kezdve lehetőség van hiba-bemutatók párjának definiálására: + +```neon +application: + errorPresenter: + 4xx: Error4xx # Nette\Application\BadRequestException esetében + 5xx: Error5xx # egyéb kivételek esetén +``` A `silentLinks` opció határozza meg, hogy a Nette hogyan viselkedjen fejlesztői módban, ha a linkgenerálás sikertelen (például mert nincs prezenter stb.). Az alapértelmezett `false` érték azt jelenti, hogy a Nette a `E_USER_WARNING` opciót váltja ki. A `true` beállítása elnyomja ezt a hibaüzenetet. Gyártási környezetben a `E_USER_WARNING` mindig meghívásra kerül. Ezt a viselkedést a [$invalidLinkMode |creating-links#Invalid Links] prezenter változó beállításával is befolyásolhatjuk. -A [leképezés meghatározza azokat a szabályokat |modules#mapping], amelyek alapján az osztály neve a prezenter nevéből származik. +Az [álnevek leegyszerűsítik a |creating-links#aliases] gyakran használt [előadókra való hivatkozást |creating-links#aliases]. + +A [leképezés meghatározza azokat a szabályokat |directory-structure#Presenter Mapping], amelyek alapján az osztály neve a bemutató nevéből származik. Az előadók automatikus regisztrációja .[#toc-automatic-registration-of-presenters] @@ -82,6 +95,9 @@ latte: # engedélyezi a [generált kód ellenőrzését |latte:develop#Checking Generated Code] phpLinter: ... # (string) alapértelmezett a null + # beállítja a nyelvjárást + locale: cs_CZ # (string) alapértelmezett érték nulla + # $this->template osztálya templateClass: # alapértelmezett értéke Nette\Bridges\ApplicationLatte\DefaultTemplate ``` @@ -91,7 +107,7 @@ Ha a Latte 3. verzióját használja, akkor új [bővítményt |latte:creating-e ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` /--comment diff --git a/application/hu/creating-links.texy b/application/hu/creating-links.texy index 8f7c11af58..e5b430a610 100644 --- a/application/hu/creating-links.texy +++ b/application/hu/creating-links.texy @@ -38,7 +38,7 @@ Lehetőség van nevesített paraméterek átadására is. A következő hivatkoz detail ``` -Ha a `ProductPresenter::renderShow()` metódus szignatúrájában nem szerepel a `$lang`, akkor a paraméter értékét a `$lang = $this->getParameter('lang')` segítségével tudja beolvasni. +Ha a `ProductPresenter::renderShow()` metódus aláírásában nem szerepel a `$lang`, akkor a paraméter értékét a `$lang = $this->getParameter('lang')` segítségével vagy a [tulajdonságból |presenters#Request Parameters] tudja lekérdezni. Ha a paraméterek egy tömbben vannak tárolva, akkor a `...` operátorral (vagy a Latte 2.x-ben a `(expand)` operátorral) bővíthetők: @@ -103,7 +103,7 @@ Ha a művelet a `default`, elhagyhatjuk, de a kettőspontnak meg kell maradnia: home ``` -A linkek más [modulokra |modules] is mutathatnak. Itt a linkeket megkülönböztetjük az almodulokhoz viszonyított, illetve abszolút linkekre. Az elv analóg a lemezes elérési utakkal, csak a kötőjelek helyett kettőspontok vannak. Tegyük fel, hogy a tényleges bemutató a `Front` modul része, akkor azt írjuk: +A hivatkozások más [modulokra |directory-structure#Presenters and Templates] is mutathatnak. Itt a linkek az almodulokhoz viszonyítva, illetve abszolút módon különböztethetők meg. Az elv analóg a lemezes elérési utakkal, csak a kötőjelek helyett kettőspontok vannak. Tegyük fel, hogy a tényleges bemutató a `Front` modul része, akkor azt írjuk: ```latte link to Front:Shop:Product:show @@ -140,7 +140,7 @@ A cél `this` létrehoz egy linket az aktuális oldalra: refresh ``` -Ugyanakkor az összes, az aláírásában megadott paramétert a `render()` vagy `action()` metódusban megadott paraméterek átadásra kerülnek. Tehát ha a `Product:show` és a `id:123` oldalakon vagyunk, akkor a `this` link is átadja ezt a paramétert. +Ugyanakkor az összes paraméter, amelyet a `action()` vagy a `render()` metódusban megadott paraméterek, ha a `action()` nincs definiálva, átadásra kerül. Tehát ha a `Product:show` és a `id:123` oldalakon vagyunk, akkor a `this` linkje is átadja ezt a paramétert. Természetesen lehetőség van a paraméterek közvetlen megadására is: @@ -213,7 +213,7 @@ Mivel a [komponensek |components] különálló, újrafelhasználható egységek Ha a komponenssablonban előadókra akarunk hivatkozni, akkor a `{plink}` címkét használjuk: ```latte -home +home ``` vagy a kódban @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Álnevek .[#toc-aliases]{data-version:v3.2.2} +============================================ + +Néha hasznos, ha egy könnyen megjegyezhető álnevet rendelünk egy Presenter:action pároshoz. Például a `Front:Home:default` honlapot egyszerűen elnevezheti `home` -nek, vagy a `Admin:Dashboard:default` -t `admin`-nak. + +Az aliasok a [konfigurációban |configuration] a `application › aliases` kulcs alatt kerülnek meghatározásra: + +```neon +application: + aliases: + home: Front:Home:default + admin: Admin:Dashboard:default + sign: Front:Sign:in +``` + +A hivatkozásokban az at szimbólummal íródnak, például: + +```latte +administration +``` + +A linkekkel dolgozó összes módszer támogatja őket, például a `redirect()` és hasonlók. + + Érvénytelen hivatkozások .[#toc-invalid-links] ============================================== @@ -257,6 +281,6 @@ Hogyan hozhatunk létre linkeket a `link()` comfort módszerrel, de prezenter je LinkGenerator egy olyan szolgáltatás, amelyet átadhat a konstruktoron keresztül, majd linkeket hozhat létre a `link()` módszerével. -Van egy különbség a bemutatókhoz képest. A LinkGenerator minden linket abszolút URL-ként hoz létre. Továbbá nincs "aktuális bemutató", így nem lehet csak a `link('default')` művelet nevét vagy a [modulok |modules] relatív elérési útvonalait megadni. +Az előadókhoz képest van különbség. A LinkGenerator minden linket közvetlenül abszolút URL-ként hoz létre. Emellett nincs "tényleges prezenter", így nem lehet csak a `link('default')` műveletnevet célként felsorolni, vagy relatív elérési utakat felsorolni a modulokhoz. Az érvénytelen linkek mindig dobnak `Nette\Application\UI\InvalidLinkException`. diff --git a/application/hu/directory-structure.texy b/application/hu/directory-structure.texy new file mode 100644 index 0000000000..26e2053554 --- /dev/null +++ b/application/hu/directory-structure.texy @@ -0,0 +1,526 @@ +Az alkalmazás könyvtárszerkezete +******************************** + +
    + +Hogyan tervezzünk világos és skálázható könyvtárstruktúrát a projektek számára a Nette Frameworkben? Megmutatjuk a bevált gyakorlatokat, amelyek segítenek a kódod rendszerezésében. Megtanulhatod: + +- hogyan kell **logikailag strukturálni** az alkalmazást könyvtárakra osztva +- hogyan tervezze meg a struktúrát úgy, hogy **jól skálázható** legyen a projekt növekedésével együtt +- melyek a **lehetséges alternatívák** és azok előnyei, illetve 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. + + +Alapvető projektstruktúra .[#toc-basic-project-structure] +========================================================= + +Bár a Nette Framework nem diktál semmilyen rögzített könyvtárszerkezetet, van 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ár +├── assets/ ← SCSS, JS fájlok, képek..., alternatívaként resources/ +├── bin/ ← parancssori szkriptek +├── config/ ← konfiguráció +├── log/ ← naplózott hibák +├── temp/ ← ideiglenes fájlok, gyorsítótár +├── tests/ ← tesztek +├── vendor/ ← a Composer által telepített könyvtárak +└── www/ ← nyilvános könyvtár (document-root) +\-- + +Ezt a struktúrát szabadon módosíthatja igényei szerint - átnevezhet vagy áthelyezhet mappákat. Ezután csak a `Bootstrap.php` és esetleg a `composer.json` könyvtárak relatív elérési útvonalait kell beállítania. Semmi másra nincs szükség, nincs bonyolult átkonfigurálás, nincs állandó változtatás. A Nette intelligens automatikus felismeréssel rendelkezik, és automatikusan felismeri az alkalmazás helyét, beleértve annak URL-bázisát is. + + +Kódszervezési elvek .[#toc-code-organization-principles] +======================================================== + +Amikor először fedez fel egy új projektet, gyorsan tájékozódnia kell. Képzelje el, hogy rákattint a `app/Model/` könyvtárra, és ezt a struktúrát látja: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +Ebből csak annyit tudsz meg, hogy a projekt használ néhány szolgáltatást, tárolót és entitást. Az alkalmazás tényleges céljáról semmit sem fog megtudni. + +Nézzünk meg egy másik megközelítést - **szervezés tartományok szerint**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Ez más - első pillantásra egyértelmű, hogy ez egy e-kereskedelmi oldal. Már a könyvtárnevek is elárulják, hogy mit tud az alkalmazás - fizetésekkel, megrendelésekkel és termékekkel dolgozik. + +Az első megközelítés (osztályok típusa szerinti szervezés) a gyakorlatban számos problémát okoz: a logikailag összefüggő kódok különböző mappákban vannak szétszórva, és ezek között ugrálni kell. Ezért szervezünk tartományok szerint. + + +Névterek .[#toc-namespaces] +--------------------------- + +Hagyományos, hogy a könyvtárszerkezet megfelel az alkalmazás névtereinek. Ez azt jelenti, hogy a fájlok fizikai helye megfelel a névterüknek. Például egy osztály, amely a `app/Model/Product/ProductRepository.php` cím alatt található, a `App\Model\Product` névtérben kell, hogy legyen. Ez az elv segíti a kódorientációt és egyszerűsíti az automatikus betöltést. + + +Egyes szám vs. többes szám a nevekben .[#toc-singular-vs-plural-in-names] +------------------------------------------------------------------------- + +Vegyük észre, hogy a fő alkalmazási könyvtárak esetében egyes számot használunk: `app`, `config`, `log`, `temp`, `www`. Ugyanez vonatkozik az alkalmazáson belül is: `Model`, `Core`, `Presentation`. Ez azért van így, mert mindegyik egy egységes fogalmat képvisel. + +Hasonlóképpen, a `app/Model/Product` mindent képvisel a termékekről. Nem nevezzük `Products`, mert ez nem egy termékekkel teli mappa (amely olyan fájlokat tartalmazna, mint `iphone.php`, `samsung.php`). Ez egy névtér, amely a termékekkel való munkához szükséges osztályokat tartalmazza - `ProductRepository.php`, `ProductService.php`. + +A `app/Tasks` mappa többes számban van, mert önállóan futtatható szkripteket tartalmaz - `CleanupTask.php`, `ImportTask.php`. Ezek mindegyike önálló egységet alkot. + +A következetesség érdekében javasoljuk a következőket használni: +- Egyes szám a funkcionális egységet képviselő névterek esetében (még akkor is, ha több egységgel dolgozunk). +- többes szám a független egységek gyűjteményeire +- Bizonytalanság esetén, vagy ha nem akarunk ezen gondolkodni, válasszuk az egyes számot. + + +Nyilvános címtár `www/` .[#toc-public-directory-www] +==================================================== + +Ez a könyvtár az egyetlen, amely a világhálóról elérhető (úgynevezett dokumentum-gyökér). Gyakran találkozhatsz a `public/` névvel a `www/` helyett - ez csak konvenció kérdése, és nem befolyásolja a funkcionalitást. A könyvtár a következőket tartalmazza: +- Az alkalmazás [belépési pontja |bootstrap#index.php] `index.php` +- `.htaccess` fájl mod_rewrite szabályokkal (Apache esetén) +- Statikus fájlok (CSS, JavaScript, képek) +- Feltöltött fájlok + +A megfelelő alkalmazásbiztonság érdekében elengedhetetlen a megfelelően [konfigurált document-root |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url]. + +.[note] +Soha ne helyezze a `node_modules/` mappát ebbe a könyvtárba - ez több ezer olyan fájlt tartalmaz, amelyek futtathatóak lehetnek, és nem lehetnek nyilvánosan hozzáférhetők. + + +Alkalmazási könyvtár `app/` .[#toc-application-directory-app] +============================================================= + +Ez a fő könyvtár az alkalmazáskóddal. Alapvető struktúra: + +/--pre +app/ +├── Core/ ← infrastrukturális kérdések +├── Model/ ← üzleti logika +├── Presentation/ ← prezenterek és sablonok +├── Tasks/ ← parancsszkriptek +└── Bootstrap.php ← alkalmazás bootstrap osztály +\-- + +`Bootstrap.php` az [alkalmazás indító osztálya |bootstrap], 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észletesen az egyes alkönyvtárakat. + + +Előadók és sablonok .[#toc-presenters-and-templates] +==================================================== + +Az alkalmazás prezentációs része a `app/Presentation` könyvtárban található. Alternatív megoldás a rövid `app/UI`. Ez a hely az összes prezenter, a sablonjaik és az esetleges segédosztályok helye. + +Ezt a réteget tartományok szerint szervezzük. Egy összetett projektben, amely egyesíti az e-kereskedelmet, a blogot és az API-t, a struktúra így nézne ki: + +/--pre +app/Presentation/ +├── Shop/ ← e-kereskedelmi 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 ezt a struktúrát használnánk: + +/--pre +app/Presentation/ +├── Front/ ← weboldal frontend +│ ├── Home/ +│ └── Post/ +├── Admin/ ← adminisztráció +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, sitemaps stb. +\-- + +A `Home/` vagy a `Dashboard/` mappák tartalmazzák az előadókat és a sablonokat. Az olyan mappák, mint a `Front/`, `Admin/` vagy `Api/` a **modulok**. Technikailag ezek szabályos könyvtárak, amelyek az alkalmazás logikai szervezését szolgálják. + +Minden bemutatót tartalmazó mappa tartalmaz egy hasonló nevű bemutatót és annak sablonjait. Például a `Dashboard/` mappa a következőket tartalmazza: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← előadó +└── default.latte ← sablon +\-- + +Ez a könyvtárszerkezet tükröződik az osztályok névterében. Például a `DashboardPresenter` a `App\Presentation\Admin\Dashboard` névtérben található (lásd [az előadói leképezést |#presenter mapping]): + +```php +namespace App\Presentation\Admin\Dashboard; + +class DashboardPresenter extends Nette\Application\UI\Presenter +{ + //... +} +``` + +A `Dashboard` prezenterre a `Admin` modulon belül az alkalmazásban a `Admin:Dashboard` kettőspont jelöléssel -ként hivatkozunk. A `default` akciójára ezután `Admin:Dashboard:default`. Beágyazott modulok esetén több kettőspontot használunk, például `Shop:Order:Detail:default`. + + +Rugalmas struktúrafejlesztés .[#toc-flexible-structure-development] +------------------------------------------------------------------- + +Ennek a struktúrának az egyik nagy előnye, hogy milyen elegánsan alkalmazkodik a növekvő projektigényekhez. Példaként vegyük az XML feedeket generáló részt. Kezdetben van egy egyszerű űrlapunk: + +/--pre +Export/ +├── ExportPresenter.php ← egy előadó minden exportra +├── sitemap.latte ← sablon az oldaltérképhez +└── feed.latte ← sablon RSS feedhez +\-- + +Idővel több feed-típus kerül hozzá, és több logikára van szükségünk... Nem probléma! A `Export/` mappából egyszerűen modul lesz: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← takarmány az Amazon számára + └── ebay.latte ← takarmány az eBay számára +\-- + +Ez az átalakítás teljesen zökkenőmentes - csak hozzon létre új almappákat, ossza fel a kódot ezekre, és frissítse a hivatkozásokat (pl. `Export:feed` -ról `Export:Feed:amazon`-re ). Ennek köszönhetően a struktúrát fokozatosan, igény szerint bővíthetjük, a beágyazási szintnek semmilyen korlátja nincs. + +Például, ha az adminisztrációban sok, a rendeléskezeléssel kapcsolatos bemutató van, mint például `OrderDetail`, `OrderEdit`, `OrderDispatch` stb, akkor a jobb szervezés érdekében létrehozhatunk egy `Order` modult (mappát), amely tartalmazni fogja a `Detail`, `Edit`, `Dispatch` és egyéb bemutatók (mappáit). + + +Sablon helye .[#toc-template-location] +-------------------------------------- + +Az előző példákban láttuk, hogy a sablonok közvetlenül az előadó mappájában találhatók: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← előadó +├── DashboardTemplate.php ← opcionális sablon osztály +└── default.latte ← sablon +\-- + +A gyakorlatban ez a hely bizonyul a legkényelmesebbnek - minden kapcsolódó fájl kéznél van. + +Alternatívaként a sablonokat a `templates/` almappában is elhelyezheti. A Nette mindkét változatot támogatja. 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#Template Lookup] fejezetben. + + +Segédosztályok és komponensek .[#toc-helper-classes-and-components] +------------------------------------------------------------------- + +A prezenterek és sablonok gyakran más segédfájlokat is tartalmaznak. Ezeket logikusan a hatókörüknek megfelelően helyezzük el: + +1. ** Közvetlenül a prezenterhez** az adott prezenterhez tartozó speciális komponensek esetén: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← komponens a terméklistához +└── FilterForm.php ← szűrő űrlap +\-- + +2. **Modulhoz** - javasoljuk a `Accessory` mappát, amely szépen az ábécé elején található: + +/--pre +Front/ +├── Accessory/ +│ ├── NavbarControl.php ← komponensek frontendhez +│ └── TemplateFilters.php +├── Product/ +└── Cart/ +\-- + +3. **A teljes alkalmazáshoz** - a `Presentation/Accessory/`: +/--pre +app/Presentation/ +├── Accessory/ +│ ├── LatteExtension.php +│ └── TemplateFilters.php +├── Front/ +└── Admin/ +\-- + +Vagy elhelyezhet segédosztályokat, mint például `LatteExtension.php` vagy `TemplateFilters.php` az infrastruktúra mappában `app/Core/Latte/`. A komponenseket pedig a `app/Components`. A választás a csapat konvencióitól függ. + + +Modell - az alkalmazás szíve .[#toc-model-heart-of-the-application] +=================================================================== + +A modell tartalmazza az alkalmazás teljes üzleti logikáját. Szervezésére ugyanaz a szabály érvényes - tartományok szerint strukturáljuk: + +/--pre +app/Model/ +├── Payment/ ← minden a fizetésekről +│ ├── PaymentFacade.php ← fő belépési pont +│ ├── PaymentRepository.php +│ ├── Payment.php ← entitás +├── Order/ ← minden a megrendelésekről +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← minden a szállításról +\-- + +A modellben jellemzően ilyen típusú osztályokkal találkozunk: + +**Arcok**: az alkalmazás egy adott tartományának fő belépési pontját jelentik. Olyan 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 használati esetek (mint például a "megrendelés létrehozása" vagy a "fizetés feldolgozása") megvalósítása érdekében. Az orkesztrációs rétegük alatt a homlokzat elrejti a megvalósítás részleteit az alkalmazás többi része elől, így egy tiszta felületet biztosít az adott területtel való munkához. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // érvényesítés + // rendelés létrehozása + // e-mail küldés + // statisztikák írása + } +} +``` + +**Szolgáltatások**: egy tartományon belüli konkrét üzleti műveletekre összpontosítanak. Ellentétben a teljes felhasználási eseteket hangszerelő homlokzatokkal, egy szolgáltatás konkrét üzleti logikát valósít meg (például árkalkulációkat vagy fizetési folyamatokat). A szolgáltatások jellemzően állapot nélküliek, és vagy a homlokzatok használhatják őket építőelemként a bonyolultabb műveletekhez, vagy közvetlenül az alkalmazás más részei az egyszerűbb feladatokhoz. + +```php +class PricingService +{ + public function calculateTotal(Order $order): Money + { + // árkalkuláció + } +} +``` + +**Repozitóriumok**: kezelik az adattárolóval, jellemzően egy adatbázissal folytatott kommunikációt. Feladatuk az entitások betöltése és mentése, valamint a keresésükhöz szükséges módszerek implementálása. A tároló megvédi az alkalmazás többi részét az adatbázis megvalósításának részleteitől, és objektumorientált felületet biztosít az adatokkal való munkához. + +```php +class OrderRepository +{ + public function find(int $id): ?Order + { + } + + public function findByCustomer(int $customerId): array + { + } +} +``` + +**Entitások**: az alkalmazás fő üzleti fogalmait reprezentáló objektumok, amelyeknek megvan az identitásuk és idővel változnak. Ezek jellemzően ORM (mint például a Nette Database Explorer vagy a Doctrine) segítségével adatbázis táblákra leképezett osztályok. Az entitások tartalmazhatnak az adataikra és az érvényesítési logikára vonatkozó üzleti szabályokat. + +```php +// Az adatbázisban szereplő táblára leképezett entitás rendelések +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 saját identitás nélküli értékeket képviselnek - például egy pénzösszeget vagy egy e-mail címet. Egy értékobjektum két példánya azonos értékekkel azonosnak tekinthető. + + +Infrastruktúra kód .[#toc-infrastructure-code] +============================================== + +A `Core/` mappa (vagy más néven `Infrastructure/`) ad otthont az alkalmazás technikai alapjának. Az infrastrukturális kód jellemzően a következőket tartalmazza: + +/--pre +app/Core/ +├── Router/ ← útválasztás és URL-kezelés +│ └── RouterFactory.php +├── Security/ ← hitelesítés és engedélyezés +│ ├── Authenticator.php +│ └── Authorizator.php +├── Logging/ ← naplózás és felügyelet +│ ├── SentryLogger.php +│ └── FileLogger.php +├── Cache/ ← gyorsítótárazási réteg +│ └── FullPageCache.php +└── Integration/ ← integráció külső szolgáltatásokkal + ├── Slack/ + └── Stripe/ +\-- + +Kisebb projektek esetében természetesen elegendő a lapos struktúra: + +/--pre +Core/ +├── RouterFactory.php +├── Authenticator.php +└── QueueMailer.php +\-- + +Ez a kód: + +- Kezeli a technikai infrastruktúrát (útválasztás, naplózás, gyorsítótár). +- Külső szolgáltatásokat integrál (Sentry, Elasticsearch, Redis). +- Alapvető szolgáltatásokat nyújt az egész alkalmazás számára (levelezés, adatbázis). +- Többnyire független az adott tartománytól - a cache vagy a logger ugyanúgy működik az e-kereskedelemben vagy a blogban. + +Kíváncsi, hogy egy bizonyos osztály ide vagy a modellbe tartozik-e? A legfontosabb különbség az, hogy a `Core/`: + +- Semmit sem tud a domainről (termékek, rendelések, cikkek). +- Általában átvihető egy másik projektbe +- Azt oldja meg, hogy "hogyan működik" (hogyan kell levelet küldeni), nem pedig azt, hogy "mit csinál" (milyen levelet kell küldeni). + +Példa a jobb megértéshez: + +- `App\Core\MailerFactory` - létrehozza az e-mail küldő osztály példányait, kezeli az SMTP beállításokat. +- `App\Model\OrderMailer` - a `MailerFactory` -t használja a megrendelésekről szóló e-mailek küldésére, ismeri a sablonjaikat és azt, hogy mikor kell elküldeni őket. + + +Parancsszkriptek .[#toc-command-scripts] +======================================== + +Az alkalmazásoknak gyakran kell a szokásos HTTP-kéréseken kívüli feladatokat végrehajtaniuk - legyen szó háttéradatfeldolgozásról, karbantartásról vagy időszakos feladatokról. A `bin/` könyvtárban található egyszerű parancsfájlok a végrehajtásra szolgálnak, míg a tényleges végrehajtási logika a `app/Tasks/` (vagy a `app/Commands/`) könyvtárba kerül. + +Példa: + +/--pre +app/Tasks/ +├── Maintenance/ ← karbantartási szkriptek +│ ├── CleanupCommand.php ← régi adatok törlése +│ └── DbOptimizeCommand.php ← adatbázis-optimalizálás +├── Integration/ ← integráció külső rendszerekkel +│ ├── ImportProducts.php ← importálás szállítói rendszerből +│ └── SyncOrders.php ← rendelések szinkronizálása +└── Scheduled/ ← rendszeres feladatok + ├── NewsletterCommand.php ← hírlevelek küldése + └── ReminderCommand.php ← vevői értesítések +\-- + +Mi tartozik a modellbe és mi a parancsszkriptekbe? Például egy e-mail elküldésének logikája a modell része, több ezer e-mail tömeges elküldése a `Tasks/`. + +A feladatokat általában [parancssorból |https://blog.nette.org/en/cli-scripts-in-nette-application] vagy cronon keresztül [futtatjuk |https://blog.nette.org/en/cli-scripts-in-nette-application]. HTTP-kérésen keresztül is futtathatók, de a biztonságot figyelembe kell venni. A feladatot futtató bemutatót biztosítani kell, például csak a bejelentkezett felhasználók számára, vagy erős tokennel és engedélyezett IP-címekről való hozzáféréssel. Hosszú feladatok esetén meg kell növelni a szkript időkorlátját, és a `session_write_close()` címet kell használni a munkamenet lezárásának elkerülése érdekében. + + +Egyéb lehetséges könyvtárak .[#toc-other-possible-directories] +============================================================== + +Az említett alapkönyvtárakon kívül a projekt igényeinek megfelelően más speciális mappákat is hozzáadhat. Nézzük meg a leggyakoribbakat és azok használatát: + +/--pre +app/ +├── Api/ ← A megjelenítési rétegtől független API logika +├── Database/ ← migrációs szkriptek és seederek a tesztadatokhoz +├── Components/ ← megosztott vizuális komponensek az egész alkalmazásban +├── Event/ ← hasznos, ha eseményvezérelt architektúrát használ +├── Mail/ ← e-mail sablonok és kapcsolódó logika +└── Utils/ ← segédosztályok +\-- + +A prezenterekben az egész alkalmazásban használt, megosztott vizuális komponensekhez használhatja a `app/Components` vagy a `app/Controls` mappát: + +/--pre +app/Components/ +├── Form/ ← megosztott űrlap komponensek +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← komponensek adatlistákhoz +│ └── DataGrid.php +└── Navigation/ ← navigációs elemek + ├── Breadcrumbs.php + └── Menu.php +\-- + +Ide tartoznak az összetettebb logikájú komponensek. Ha a komponenseket több projekt között szeretné megosztani, akkor érdemes azokat egy önálló composer csomagban elkülöníteni. + +A `app/Mail` könyvtárban helyezheti el az e-mail kommunikáció kezelését: + +/--pre +app/Mail/ +├── templates/ ← e-mail sablonok +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Előadótérképezés .[#toc-presenter-mapping] +========================================== + +A leképezés szabályokat határoz meg az osztályok nevének a bemutató nevéből történő származtatására. Ezeket a [konfigurációban |configuration] a `application › mapping` kulcs alatt adjuk meg. + +Ezen az oldalon megmutattuk, hogy az előadókat a `app/Presentation` mappába (vagy a `app/UI`) helyezzük el. Erről a konvencióról a konfigurációs fájlban kell tájékoztatnunk a Nette-et. Egyetlen sor elég: + +```neon +application: + mapping: App\Presentation\*\**Presenter +``` + +Hogyan működik a leképezés? A jobb megértéshez először képzeljünk el egy modulok nélküli alkalmazást. Szeretnénk, ha a prezenter osztályok a `App\Presentation` névtér alá tartoznának, így a `Home` prezenter a `App\Presentation\HomePresenter` osztályhoz tartozik. Ezt a következő konfigurációval érjük el: + +```neon +application: + mapping: App\Presentation\*Presenter +``` + +A leképezés úgy működik, hogy a `App\Presentation\*Presenter` maszkban a csillagot a `Home` prezenter névvel helyettesítjük, ami a `App\Presentation\HomePresenter` végső osztálynevet eredményezi. Egyszerű! + +Azonban, ahogy ebben és más fejezetekben található példákban is láthatjuk, a prezenter osztályokat névadó alkönyvtárakba helyezzük, például a `Home` prezenter a `App\Presentation\Home\HomePresenter` osztályra képezi le a prezentert. Ezt a kettőspont megduplázásával érjük el (Nette Application 3.2 szükséges): + +```neon +application: + mapping: App\Presentation\**Presenter +``` + +Most áttérünk a prezenterek modulokba való leképezésére. Minden egyes modulhoz sajátos leképezést határozhatunk meg: + +```neon +application: + mapping: + Front: App\Presentation\Front\**Presenter + Admin: App\Presentation\Admin\**Presenter + Api: App\Api\*Presenter +``` + +E konfiguráció szerint a `Front:Home` bemutató a `App\Presentation\Front\Home\HomePresenter` osztályhoz, míg a `Api:OAuth` bemutató a `App\Api\OAuthPresenter` osztályhoz tartozik. + +Mivel a `Front` és a `Admin` modulok hasonló leképezési módszerrel rendelkeznek, és valószínűleg több ilyen modul is lesz, létrehozható egy általános szabály, amely helyettesíti őket. Az osztálymaszkhoz egy új csillagot adunk a modulhoz: + +```neon +application: + mapping: + *: App\Presentation\*\**Presenter + Api: App\Api\*Presenter +``` + +Ez működik a mélyebben egymásba ágyazott könyvtárstruktúrák esetében is, mint például a `Admin:User:Edit` bemutató, ahol a csillaggal ellátott szegmens minden szinten megismétlődik, és a `App\Presentation\Admin\User\Edit\EditPresenter` osztályt eredményezi. + +Egy alternatív jelölés az, ha a karakterlánc helyett egy három szegmensből álló tömböt használunk. 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 index f7503c9ef1..0f8ae7c74b 100644 --- a/application/hu/how-it-works.texy +++ b/application/hu/how-it-works.texy @@ -22,18 +22,18 @@ A könyvtárszerkezet valahogy így néz ki: /--pre web-project/ ├── app/ ← directory with application -│ ├── Presenters/ ← presenter classes -│ │ ├── HomePresenter.php ← Home presenter class -│ │ └── templates/ ← templates directory -│ │ ├── @layout.latte ← template of shared layout -│ │ └── Home/ ← templates for Home presenter -│ │ └── default.latte ← template for action `default` -│ ├── Router/ ← configuration of URL addresses +│ ├── Core/ ← alapvető szükséges osztályok +│ │ └── RouterFactory.php ← URL címek konfigurálása +│ ├── Presentation/ ← prezenterek, sablonok és társai. +│ │ ├── @layout.latte ← a megosztott elrendezés sablonja +│ │ └── Home/ ← Főoldal bemutatókönyvtár +│ │ ├── HomePresenter.php ← Home prezenter osztály +│ │ └── default.latte ← cselekvési sablon default │ └── Bootstrap.php ← booting class Bootstrap ├── bin/ ← scripts for the command line ├── config/ ← configuration files │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← error logs ├── temp/ ← temporary files, cache, … ├── vendor/ ← libraries installed by Composer @@ -45,9 +45,9 @@ A könyvtárszerkezet valahogy így néz ki: └── .htaccess ← prohibits access to all directories except www \-- -A könyvtárszerkezetet bármilyen módon megváltoztathatja, átnevezhet vagy áthelyezhet mappákat, majd csak szerkesztheti a `log/` és a `temp/` elérési útvonalakat a `Bootstrap.php` fájlban, és az ehhez a fájlhoz vezető útvonalat a `composer.json` fájlban a `autoload` szakaszban. Semmi több, semmi bonyolult átkonfigurálás, semmi állandó változtatás. A Nette [intelligens automatikus felismeréssel |bootstrap#development-vs-production-mode] rendelkezik. +A könyvtárszerkezetet tetszés szerint módosíthatja, átnevezheti vagy áthelyezheti a mappákat - ez teljesen rugalmas. A Nette intelligens automatikus felismeréssel is rendelkezik, és automatikusan felismeri az alkalmazás helyét, beleértve annak URL-bázisát is. -Kicsit nagyobb alkalmazásoknál a bemutatókat és sablonokat tartalmazó mappákat alkönyvtárakra (a lemezen) és névterekre (a kódban) oszthatjuk, amelyeket [moduloknak |modules] nevezünk. +Kicsit nagyobb alkalmazások esetén a prezenter- és sablonmappákat [alkönyvtárakba |directory-structure#Presenters and templates] rendezhetjük, az osztályokat pedig névterekbe csoportosíthatjuk, amelyeket moduloknak nevezünk. A `www/` könyvtár a projekt nyilvános könyvtára vagy dokumentum-gyökere. Ezt átnevezhetjük anélkül, hogy az alkalmazás oldalán bármi mást be kellene állítanunk. Csak a [tárhelyet |nette:troubleshooting#How to change or remove www directory from URL] kell úgy [beállítania |nette:troubleshooting#How to change or remove www directory from URL], hogy a document-root ebbe a könyvtárba kerüljön. @@ -75,7 +75,7 @@ Feladata a következő: Milyen gyárat? Mi nem traktorokat gyártunk, hanem weboldalakat! Várjon, mindjárt elmagyarázzuk. -A "környezet inicializálása" alatt például azt értjük, hogy a [Tracy |tracy:] aktiválódik, ami egy csodálatos eszköz a hibák naplózására vagy vizualizálására. Naplózza a hibákat a termelő szerveren, és közvetlenül a fejlesztői szerveren jeleníti meg azokat. Ezért az inicializálásnak azt is el kell döntenie, hogy az oldal termelő vagy fejlesztői üzemmódban fut-e. Ehhez a Nette automatikus felismerést használ: ha a webhelyet localhoston futtatja, akkor fejlesztői módban fut. Nem kell semmit sem konfigurálnia, és az alkalmazás készen áll mind a fejlesztői, mind a gyártói telepítésre. Ezeket a lépéseket a [Bootstrap osztályról |bootstrap] szóló fejezetben végezzük el és ismertetjük részletesen. +A "környezet inicializálása" alatt például a [Tracy |tracy:] aktiválását értjük, amely egy fantasztikus eszköz a naplózáshoz és a hibák vizualizálásához. Termelő szervereken naplózza a hibákat, míg fejlesztői szervereken közvetlenül megjeleníti azokat. Ezért az inicializálás magában foglalja annak meghatározását, hogy a weboldal termelési vagy fejlesztési üzemmódban fut-e. Ehhez a Nette [intelligens automatikus felismerést |bootstrap#development-vs-production-mode] használ: ha a webhelyet localhoston futtatja, akkor fejlesztési üzemmódban működik. Nincs szükség konfigurációra, és az alkalmazás készen áll mind a fejlesztési, mind a termelési telepítésre. Ezeket a lépéseket a [Bootstrap osztály |bootstrap] fejezetben végezzük el és részletezzük. 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 végzi (a továbbiakban `Application`), így amikor azt mondjuk, hogy "futtassunk egy alkalmazást", akkor azt értjük, hogy hívjunk meg egy `run()` nevű metódust ennek az osztálynak egy objektumán. @@ -91,7 +91,7 @@ A Nette-ben írt alkalmazások sok úgynevezett prezenterre oszlanak (más keret Az alkalmazás azzal indul, hogy az ún. routertől kéri, hogy döntse el, hogy az aktuális kérést melyik prezenternek adja át feldolgozásra. A router dönti el, hogy kinek a felelőssége. Megnézi a bemeneti URL-t `https://example.com/product/123`, aki a `show` egy terméket `id: 123` művelettel akarja ellátni. Jó szokás a prezenter + akció párokat kettősponttal elválasztva `Product:show`-ként írni. -Tehát a router az URL-t átalakította `Presenter:action` + paraméterek párrá, esetünkben `Product:show` + `id: 123`. A `app/Router/RouterFactory.php` fájlban láthatjuk, hogyan néz ki egy útválasztó, és ezt részletesen az [Útválasztás |Routing] fejezetben fogjuk leírni. +Tehát a router az URL-t átalakította `Presenter:action` + paraméterek párrá, esetünkben `Product:show` + `id: 123`. A `app/Core/RouterFactory.php` fájlban láthatjuk, hogyan néz ki egy útválasztó, és ezt részletesen az [Útválasztás |Routing] fejezetben fogjuk leírni. Lépjünk tovább. Az alkalmazás már ismeri a bemutató nevét, és folytathatja. Egy `ProductPresenter` objektum létrehozásával, amely a `Product` bemutató kódja. Pontosabban megkéri a DI konténert a prezenter létrehozására, mert az objektumok előállítása az ő feladata. @@ -121,12 +121,9 @@ Tehát a `renderShow(123)` metódust hívtuk meg, amelynek kódja fiktív példa Ezt követően a prezenter 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 egy másik oldalra való átirányítás. Fontos, hogy ha nem mondjuk meg kifejezetten, hogyan kell válaszolni (ami a `ProductPresenter` esetében a helyzet), akkor a válasz az lesz, hogy a sablon egy HTML-oldallal jeleníti meg a sablont. Hogy miért? Nos, mert az esetek 99%-ában egy sablont szeretnénk kirajzolni, így a prezentáló ezt a viselkedést veszi alapértelmezettnek, és a mi munkánkat akarja megkönnyíteni. Ez a Nette lényege. -Még csak meg sem kell adnunk, hogy melyik sablont rajzoljuk ki, egyszerű logika szerint levezeti az oda vezető utat. A presenter `Product` és az action `show` esetében megnézi, hogy létezik-e valamelyik sablonfájl a `ProductPresenter` osztály könyvtárához képest, ahol a osztály található: +Még azt sem kell megadnunk, hogy melyik sablont kell megjelenítenünk; a keretrendszer magától levonja az útvonalat. A `show` akció esetében egyszerűen megpróbálja betölteni a `show.latte` sablont a `ProductPresenter` osztályt tartalmazó könyvtárban. Megpróbálja megtalálni az elrendezést is a `@layout.latte` fájlban (a [sablonkeresésről |templates#Template Lookup] bővebben). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -Megpróbálja megtalálni az elrendezést is a `@layout.latte` fájlban, majd rendereli a sablont. Ezzel a prezenter és az egész alkalmazás feladata befejeződött. Ha a sablon nem létezik, akkor egy 404-es hibaüzenetű oldal fog visszakerülni. A prezenterekről bővebben a [Prezenterek |Presenters] oldalon olvashat. +Ezt követően a sablonok megjelenítésre kerülnek. Ezzel a bemutató és az egész alkalmazás feladata befejeződik, és a munka elvégeztetett. Ha a sablon nem létezne, akkor egy 404-es hibaoldalt kapna vissza. A prezenterekről bővebben a [Prezenterek |presenters] oldalon olvashat. [* request-flow.svg *] @@ -137,7 +134,7 @@ A biztonság kedvéért próbáljuk meg az egész folyamatot egy kicsit más URL 3) a router dekódolja az URL-t, mint egy párat `Home:default` 4) létrejön egy `HomePresenter` objektum 5) a `renderDefault()` metódust meghívjuk (ha létezik) -6) egy `templates/Home/default.latte` sablon `templates/@layout.latte` elrendezéssel megjelenik. +6) egy `default.latte` sablon `@layout.latte` elrendezéssel megjelenik. Lehet, hogy most sok új fogalommal találkoztál, de úgy gondoljuk, hogy van értelme. Az alkalmazások létrehozása a Nette-ben gyerekjáték. diff --git a/application/hu/modules.texy b/application/hu/modules.texy deleted file mode 100644 index 5dde2d1a36..0000000000 --- a/application/hu/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Modulok -******* - -.[perex] -A Nette-ben a modulok az alkalmazást alkotó logikai egységeket jelentik. Ide tartoznak a prezenterek, sablonok, esetleg komponensek és modellosztályok. - -Egy könyvtár a bemutatóknak és egy a sablonoknak nem lenne elég a valódi projektekhez. Több tucat fájl egy mappában való elhelyezése legalábbis rendezetlen. Hogyan szabadulhatunk meg ettől? Egyszerűen felosztjuk őket alkönyvtárakra a lemezen és névterekre a kódban. És pontosan ezt teszik a Nette modulok. - -Felejtsük el tehát az egyetlen mappát az előadóknak és a sablonoknak, és helyette hozzunk létre modulokat, például a `Admin` és a `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← directory with modules -│ ├── Admin/ ← module Admin -│ │ ├── Presenters/ ← its presenters -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← module Front -│ └── Presenters/ ← its presenters -│ └── ... -\-- - -Ezt a könyvtárszerkezetet az osztályok névterei is tükrözni fogják, így például a `DashboardPresenter` a `App\Modules\Admin\Presenters` névtérben lesz: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -A `Dashboard` prezenterre a `Admin` modulon belül az alkalmazáson belül a kettőspont jelöléssel `Admin:Dashboard`, a `default` műveletre pedig `Admin:Dashboard:default` néven hivatkozunk. -És honnan tudja a Nette proper, hogy a `Admin:Dashboard` a `App\Modules\Admin\Presenters\DashboardPresenter` osztályt képviseli? Ezt a konfigurációban történő [leképezéssel |#mapping] határozzuk meg. -A megadott struktúra tehát nem keményen meghatározott, és Ön az igényeinek megfelelően módosíthatja. - -A modulok természetesen a prezentereken és sablonokon kívül minden más elemet is tartalmazhatnak, például komponenseket, modellosztályokat stb. - - -Beágyazott modulok .[#toc-nested-modules] ------------------------------------------ - -A moduloknak nem kell csak sima struktúrát alkotniuk, létrehozhatunk például almodulokat is: - -/--pre -app/ -├── Modules/ ← directory with modules -│ ├── Blog/ ← module Blog -│ │ ├── Admin/ ← submodule Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← submodule Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← module Forum -│ │ └── ... -\-- - -Így a `Blog` modul `Admin` és `Front` almodulokra oszlik. Ez ismét tükröződik a névterekben, amelyek a `App\Modules\Blog\Admin\Presenters` stb. lesznek. Az almodulon belüli `Dashboard` bemutatót `Blog:Admin:Dashboard` néven említjük. - -A beágyazás tetszőlegesen mélyre mehet, így al-almodulok hozhatók létre. - - -Linkek létrehozása .[#toc-creating-links] ------------------------------------------ - -A bemutatósablonokban lévő hivatkozások az aktuális modulhoz viszonyítva vannak. Így a `Foo:default` hivatkozás a `Foo` bemutatóhoz vezet, amely ugyanabban a modulban található, mint az aktuális bemutató. Ha az aktuális modul például a `Front`, akkor a link így néz ki: - -```latte -link to Front:Product:show -``` - -A hivatkozás akkor is relatív, ha egy modul nevét tartalmazza, amely ilyenkor almodulnak minősül: - -```latte -link to Front:Shop:Product:show -``` - -Az abszolút hivatkozások a lemezen lévő abszolút elérési utakhoz hasonlóan íródnak, de a kettőspontok helyett kettőspontokkal. Az abszolút link tehát kettősponttal kezdődik: - -```latte -link to Admin:Product:show -``` - -Hogy megtudjuk, hogy egy adott modulban vagy annak almoduljában vagyunk-e, használhatjuk a `isModuleCurrent(moduleName)` függvényt. - -```latte -
  • - ... -
  • -``` - - -Útválasztás .[#toc-routing] ---------------------------- - -Lásd [az útválasztásról szóló fejezetet |routing#Modules]. - - -Feltérképezés .[#toc-mapping] ------------------------------ - -Meghatározza azokat a szabályokat, amelyek alapján az osztály neve az előadó nevéből származik. Ezeket a [konfigurációban |configuration] a `application › mapping` kulcs alatt írjuk le. - -Kezdjük egy olyan példával, amely nem használ modulokat. Csak azt akarjuk, hogy a prezenter osztályok a `App\Presenters` névtérrel rendelkezzenek. Ez azt jelenti, hogy egy olyan prezenternek, mint a `Home`, a `App\Presenters\HomePresenter` osztályhoz kell kapcsolódnia. Ezt a következő konfigurációval érhetjük el: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -Az osztálymaszkban a prezenter nevét csillaggal helyettesítjük, és az eredmény az osztály neve lesz. Easy! - -Ha az előadókat modulokra osztjuk, akkor minden modulhoz saját leképezésünk lehet: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Most a `Front:Home` bemutatót a `App\Modules\Front\Presenters\HomePresenter` osztályra, a `Admin:Dashboard` bemutatót pedig a `App\Modules\Admin\Presenters\DashboardPresenter` osztályra képezzük le. - -Praktikusabb egy általános (csillag) szabályt létrehozni az első kettő helyett. Az extra csillagot csak a modul számára adjuk hozzá az osztálymaszkhoz: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -De mi van akkor, ha egymásba ágyazott modulokat használunk, és van egy bemutató `Admin:User:Edit`? Ebben az esetben a modult jelképező csillaggal ellátott szegmens minden szinten egyszerűen megismétlődik, és az eredmény a `App\Modules\Admin\User\Presenters\EditPresenter` osztály lesz. - -Egy alternatív jelölés az, hogy a karakterlánc helyett egy három szegmensből álló tömböt használunk. Ez a jelölés egyenértékű az előzővel: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -Az alapértelmezett érték a `*: *Module\*Presenter`. diff --git a/application/hu/presenters.texy b/application/hu/presenters.texy index 3dadaaf424..076c632a4c 100644 --- a/application/hu/presenters.texy +++ b/application/hu/presenters.texy @@ -60,7 +60,7 @@ Hasonlóan a módszerhez `render()`. Míg a `render()` célja, hogy Fontos, hogy `action()` előbb hívódik meg, mint a `render()`, így ezen belül esetleg megváltoztathatjuk az életciklus következő menetét, azaz megváltoztathatjuk a megjelenítendő sablont és a metódust is. `render()` ami meghívásra kerül, a `setView('otherView')` segítségével. -A kérésből származó paramétereket átadjuk a metódusnak. Lehetséges és ajánlott a paraméterek típusainak megadása, pl. `actionShow(int $id, string $slug = null)` - ha a `id` paraméter hiányzik, vagy nem egész szám, a prezenter [404-es hibát |#Error 404 etc.] ad vissza, és megszakítja a műveletet. +A kérésből származó paramétereket átadjuk a metódusnak. Lehetséges és ajánlott a paraméterek típusainak megadása, pl. `actionShow(int $id, ?string $slug = null)` - ha a `id` paraméter hiányzik, vagy nem egész szám, a prezenter [404-es hibát |#Error 404 etc.] ad vissza, és megszakítja a műveletet. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ A sablonban ezek az üzenetek a `$flashes` változóban `stdClass` objektumként 404-es hiba stb. .[#toc-error-404-etc] ====================================== -Ha nem tudjuk teljesíteni a kérést, mert például a megjeleníteni kívánt cikk nem létezik az adatbázisban, akkor a 404-es hibát dobjuk ki a `error(string $message = null, int $httpCode = 404)` módszerrel, amely a 404-es HTTP hibát jelenti: +Ha nem tudjuk teljesíteni a kérést, mert például a megjeleníteni kívánt cikk nem létezik az adatbázisban, akkor a 404-es hibát dobjuk ki a `error(?string $message = null, int $httpCode = 404)` módszerrel, amely a 404-es HTTP hibát jelenti: ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Kérési paraméterek .[#toc-request-parameters] +============================================= + +A bemutató, valamint minden komponens a HTTP-kérésből kapja a paramétereit. Értékeik a `getParameter($name)` módszerrel vagy a `getParameters()` segítségével kérhetők le. Az értékek karakterláncok vagy karakterláncok tömbjei, lényegében közvetlenül az URL-ből nyert nyers adatok. + +A további kényelem érdekében javasoljuk, hogy a paraméterek tulajdonságokon keresztül legyenek elérhetők. Egyszerűen jegyzeteljük őket 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; // nyilvánosnak kell lennie +} +``` + +A tulajdonságok esetében javasoljuk, hogy adja meg az adattípust (pl. `string`). A Nette ezután automatikusan ennek alapján alakítja ki az értéket. A paraméterek értékei is [érvényesíthetők |#Validation of Parameters]. + +A hivatkozás létrehozásakor közvetlenül megadhatja a paraméterek értékét: + +```latte +click +``` + + Tartós paraméterek .[#toc-persistent-parameters] ================================================ @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Ha a `$this->lang` értéke például `'en'`, akkor a `link()` vagy `n:href` használatával létrehozott linkek a `lang=en` paramétert is tartalmazni fogják. És amikor a linkre kattintunk, akkor ismét a `$this->lang = 'en'` lesz. -A tulajdonságok esetében javasoljuk, hogy adja meg az adattípust (pl. `string`), és egy alapértelmezett értéket is megadhat. A paraméterek értékei [érvényesíthetők |#Validation of Persistent Parameters]. +A tulajdonságok esetében javasoljuk, hogy adja meg az adattípust (pl. `string`), és megadhatja az alapértelmezett értéket is. A paraméterek értékei [érvényesíthetők |#Validation of Parameters]. A perzisztens paraméterek alapértelmezés szerint egy adott bemutató összes művelete között átadásra kerülnek. Több prezenter között történő átadásukhoz vagy meg kell határozni őket: @@ -307,18 +333,12 @@ Mélyebbre ásva .[#toc-going-deeper] Amit eddig ebben a fejezetben bemutattunk, valószínűleg elegendő lesz. A következő sorok azoknak szólnak, akiket a prezenterek mélységében érdekelnek, és mindent tudni akarnak. -Követelmények és paraméterek .[#toc-requirement-and-parameters] ---------------------------------------------------------------- - -A bemutató által kezelt kérés a [api:Nette\Application\Request] objektum, amelyet a bemutató `getRequest()` metódusa ad vissza. Ez paraméterek tömbjét tartalmazza, és mindegyik paraméter vagy valamelyik komponenshez, vagy közvetlenül a prezentálóhoz tartozik (amely valójában szintén egy komponens, bár egy speciális komponens). A Nette tehát a `loadState(array $params)` metódus meghívásával újraosztja a paramétereket és átadja az egyes komponensek (és a prezenter) között. A paramétereket a `getParameters(): array` metódussal lehet megszerezni, egyenként a `getParameter($name)` segítségével. A paraméterek értékei karakterláncok vagy karakterláncok tömbjei, alapvetően közvetlenül az URL-ből nyert nyers adatok. +A paraméterek validálása .[#toc-validation-of-parameters] +--------------------------------------------------------- +Az URL-ekből kapott [kérési paraméterek |#request parameters] és [állandó paraméterek |#persistent parameters] értékeit a `loadState()` módszer írja a tulajdonságokba. Azt is ellenőrzi, hogy a tulajdonságban megadott adattípus megfelel-e, ellenkező esetben 404-es hibával válaszol, és az oldal nem jelenik meg. -A tartós paraméterek validálása .[#toc-validation-of-persistent-parameters] ---------------------------------------------------------------------------- - -Az URL-ekből kapott [állandó paraméterek |#persistent parameters] értékeit a `loadState()` módszer írja a tulajdonságokba. A módszer azt is ellenőrzi, hogy a tulajdonságban megadott adattípus megfelel-e, ellenkező esetben 404-es hibával válaszol, és az oldal nem jelenik meg. - -Soha ne bízzunk vakon a perzisztens paraméterekben, mivel azokat a felhasználó könnyen felülírhatja az URL-ben. Például így ellenőrizzük, hogy a `$this->lang` szerepel-e a támogatott nyelvek között. Jó megoldás erre a fent említett `loadState()` metódus felülírása: +Soha ne bízzon vakon a paraméterekben, mert azokat a felhasználó könnyen felülírhatja az URL-ben. Például így ellenőrizzük, hogy a `$this->lang` szerepel-e a támogatott nyelvek között. Ennek jó módja a fent említett `loadState()` metódus felülírása: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,7 +361,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter A kérelem mentése és visszaállítása .[#toc-save-and-restore-the-request] ------------------------------------------------------------------------ -Az aktuális kérést elmentheti egy munkamenetbe, vagy visszaállíthatja a munkamenetből, és hagyhatja, hogy az előadó újra végrehajtsa azt. Ez például akkor hasznos, ha egy felhasználó kitölt egy űrlapot, és a bejelentkezése lejár. Annak érdekében, hogy ne veszítsünk el adatokat, a bejelentkezési oldalra való átirányítás előtt az aktuális kérést a munkamenetbe mentjük a `$reqId = $this->storeRequest()` segítségével, amely egy rövid karakterlánc formájában visszaad egy azonosítót, és paraméterként átadja a bejelentkezési prezenternek. +A kérés, amelyet a bemutató kezel, egy objektum [api:Nette\Application\Request] és a bemutató `getRequest()` metódusa adja vissza. + +Az aktuális kérést elmentheti egy munkamenetbe, vagy visszaállíthatja a munkamenetből, és hagyhatja, hogy a prezenter ismét futtassa. Ez például akkor hasznos, ha egy felhasználó kitölt egy űrlapot, és a bejelentkezése lejár. Annak érdekében, hogy ne veszítsünk el adatokat, a bejelentkezési oldalra való átirányítás előtt az aktuális kérést a munkamenetbe mentjük a `$reqId = $this->storeRequest()` segítségével, amely egy rövid karakterlánc formájában visszaad egy azonosítót, és paraméterként átadja a bejelentkezési prezenternek. A bejelentkezés után meghívjuk a `$this->restoreRequest($reqId)` metódust, amely átveszi a kérést a munkamenetből, és továbbítja azt. A módszer ellenőrzi, hogy a kérést ugyanaz a felhasználó hozta-e létre, mint aki most bejelentkezett. Ha egy másik felhasználó jelentkezik be, vagy a kulcs érvénytelen, akkor nem tesz semmit, és a program folytatódik. @@ -362,7 +384,7 @@ Az átirányítás nem történik AJAX vagy POST kérés esetén, mivel az adatv A kanonizálás manuálisan is meghívható a `canonicalize()` módszerrel, amely a `link()` módszerhez hasonlóan a prezentálót, a műveleteket és a paramétereket kapja argumentumként. Létrehoz egy linket, és összehasonlítja azt az aktuális URL-lel. Ha eltér, akkor átirányít a létrehozott linkre. ```php -public function actionShow(int $id, string $slug = null): void +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. @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Hozzáférés korlátozása `#[Requires]` .[#toc-access-restriction-using-requires]{data-version:3.2.2} +-------------------------------------------------------------------------------------------------- + +A `#[Requires]` attribútum speciális lehetőségeket biztosít az előadókhoz és módszereikhez való hozzáférés korlátozására. Használható HTTP-módszerek megadására, AJAX-kérések megkövetelésére, az azonos eredetű hozzáférések korlátozására és a hozzáférésnek csak a továbbításra való korlátozására. Az attribútum alkalmazható a prezenter osztályokra, valamint az egyes metódusokra, mint például a `action()`, `render()`, `handle()`, és `createComponent()`. + +Ezeket a korlátozásokat megadhatja: +- a HTTP-módszerekre: `#[Requires(methods: ['GET', 'POST'])]` +- AJAX-kérést igényel: `#[Requires(ajax: true)]` +- hozzáférés csak ugyanabból az eredetből: `#[Requires(sameOrigin: true)]` +- hozzáférés csak továbbítással: `#[Requires(forward: true)]` +- korlátozások bizonyos műveletekre: `#[Requires(actions: 'default')]` + +A részletekért lásd [Hogyan használjuk a Requires attribútum használata |best-practices:attribute-requires]. + + +HTTP módszer ellenőrzése .[#toc-http-method-check] +-------------------------------------------------- + +A Nette rendszerben az előadók elsősorban biztonsági okokból automatikusan ellenőrzik minden bejövő kérés HTTP-módszerét. Alapértelmezés szerint a `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` módszerek engedélyezettek. + +Ha további módszereket, például a `OPTIONS`, szeretne engedélyezni, akkor használhatja a `#[Requires]` attribútumot (a 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ést a `checkHttpMethod()` végzi, amely ellenőrzi, hogy a kérelemben megadott módszer szerepel-e a `$presenter->allowedMethods` tömbben. Adjon hozzá egy ilyen módszert: + +```php +class MyPresenter extends Nette\Application\UI\Presenter +{ + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } +} +``` + +Fontos hangsúlyozni, hogy ha engedélyezi a `OPTIONS` módszert, akkor azt megfelelően kell kezelnie a prezenteren belül is. Ezt a módszert gyakran használják úgynevezett preflight-kérelemként, amelyet a böngészők automatikusan elküldenek a tényleges kérés előtt, amikor meg kell határozni, hogy a kérés engedélyezett-e a CORS (Cross-Origin Resource Sharing) irányelv szempontjából. Ha engedélyezi ezt a módszert, de nem valósít meg megfelelő választ, az következetlenségekhez és potenciális biztonsági problémákhoz vezethet. + + További olvasmányok .[#toc-further-reading] =========================================== diff --git a/application/hu/routing.texy b/application/hu/routing.texy index 5cf399296b..1a88490e17 100644 --- a/application/hu/routing.texy +++ b/application/hu/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Vagy használhatjuk ezt a formát, vegyük észre az érvényesítési reguláris kifejezés átírását: +Részletesebb specifikációhoz egy még bővebb formát lehet használni, ahol az alapértelmezett értékek mellett más paramétertulajdonságok is megadhatók, például egy érvényesítési reguláris kifejezés (lásd a `id` paramétert): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Ezek a beszédesebb formátumok hasznosak más metaadatok hozzáadásához. +Fontos megjegyezni, hogy ha a tömbben definiált paraméterek nem szerepelnek az elérési útvonal maszkjában, akkor értékük nem módosítható, még az URL-ben kérdőjel után megadott lekérdezési paraméterekkel sem. Szűrők és fordítások .[#toc-filters-and-translations] @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Modulok .[#toc-modules] ----------------------- -Ha több olyan útvonalunk van, amelyek egy [modulhoz |modules] tartoznak, akkor a `withModule()` segítségével csoportosíthatjuk őket: +Ha több útvonalunk van, amelyek egy [modulhoz |directory-structure#Presenters and Templates] tartoznak, a `withModule()` segítségével csoportosíthatjuk őket: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Integráció .[#toc-integration] ============================== -Ahhoz, hogy a routerünket az alkalmazásba kapcsoljuk, tájékoztatnunk kell róla a DI konténert. A legegyszerűbb, ha elkészítjük a gyárat, amely a router objektumot fogja felépíteni, és megmondjuk a konténer konfigurációjának, hogy használja azt. Tegyük fel, hogy írunk egy metódust erre a célra `App\Router\RouterFactory::createRouter()`: +Ahhoz, hogy a routerünket az alkalmazásba kapcsoljuk, tájékoztatnunk kell róla a DI konténert. A legegyszerűbb, ha elkészítjük a gyárat, amely a router objektumot fogja felépíteni, és megmondjuk a konténer konfigurációjának, hogy használja azt. Tegyük fel, hogy írunk egy metódust erre a célra `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ Ezután beírjuk a [konfigurációba |dependency-injection:services]: ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Az esetleges függőségeket, például az adatbázis-kapcsolatot stb., az [autowiring |dependency-injection:autowiring] segítségével paraméterként átadjuk a gyári metódusnak: @@ -663,7 +663,7 @@ A szeparált használat alatt a router képességeinek olyan alkalmazásban tör Tehát ismét létrehozunk egy metódust, amely létrehoz egy útválasztót, például: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Vagy közvetlenül hozzuk létre az objektumokat: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/hu/templates.texy b/application/hu/templates.texy index 71b205b7d1..491d1b5ae1 100644 --- a/application/hu/templates.texy +++ b/application/hu/templates.texy @@ -34,35 +34,81 @@ Az a szokásos, hogy az oldal az elrendezési sablonból + az akció sablonból Meghatározza a `content` blokkot, amely az elrendezésben a `{include content}` helyére kerül, és újra definiálja a `title` blokkot is, amely felülírja az elrendezésben a `{block title}` blokkot. Próbálja meg elképzelni az eredményt. -Sablonok keresése .[#toc-search-for-templates] ----------------------------------------------- +Sablon keresés .[#toc-template-lookup] +-------------------------------------- -A sablonok elérési útvonalát egyszerű logika szerint vezetjük le. Megpróbálja megnézni, hogy létezik-e valamelyik sablonfájl ahhoz a könyvtárhoz képest, ahol a prezenter osztály található, ahol a `` az aktuális prezenter neve és `` az aktuális művelet neve: +A prezenterekben nem kell megadnia, hogy melyik sablont kell megjeleníteni; a keretrendszer automatikusan meghatározza az útvonalat, megkönnyítve ezzel a kódolást. -- `templates//.latte` -- `templates/..latte` +Ha olyan könyvtárstruktúrát használ, ahol minden prezenternek saját könyvtára van, egyszerűen helyezze el a sablont ebben a könyvtárban az akció (pl. nézet) neve alatt. Például a `default` művelethez használja a `default.latte` sablont: -Ha a sablon nem található, a program megpróbál a `templates` könyvtárban keresni egy szinttel feljebb, azaz ugyanazon a szinten, mint a bemutató osztályt tartalmazó könyvtár. +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -Ha a sablon ott sem található, a válasz [404-es hiba |presenters#Error 404 etc.]. +Ha olyan struktúrát használ, ahol az előadók együttesen egy könyvtárban vannak, a sablonok pedig a `templates` mappában, mentse el vagy egy fájlban `..latte` vagy a `/.latte`: -A nézetet a `$this->setView('otherView')` segítségével is megváltoztathatja. Vagy a keresés helyett közvetlenül megadhatja a sablonfájl nevét a `$this->template->setFile('/path/to/template.latte')` segítségével. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- + +A `templates` könyvtár egy szinttel feljebb is elhelyezhető, ugyanazon a szinten, mint az előadói osztályokat tartalmazó könyvtár. + +Ha a sablon nem található, a prezenter [404 - page not found hibával |presenters#Error 404 etc] válaszol. + +A nézetet a `$this->setView('anotherView')` segítségével lehet megváltoztatni. Lehetőség van a sablonfájl közvetlen megadására is a `$this->template->setFile('/path/to/template.latte')` segítségével. .[note] -A [formatTemplateFiles |api:Nette\Application\UI\Presenter::formatTemplateFiles()] metódus felülbírálásával módosíthatja azokat az elérési utakat, ahol a sablonok keresése történik, amely a lehetséges fájl elérési utak tömbjét adja vissza. +A fájlokat, amelyekben a sablonok keresése történik, a [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()] metódus felülbírálásával lehet megváltoztatni, amely a lehetséges fájlnevek tömbjét adja vissza. + + +Layout sablon keresés .[#toc-layout-template-lookup] +---------------------------------------------------- + +A Nette automatikusan megkeresi az elrendezési fájlt is. + +Ha olyan könyvtárstruktúrát használ, ahol minden előadónak saját könyvtára van, akkor az elrendezést vagy az előadóval közös mappába helyezze el, ha csak rá jellemző, vagy egy szinttel feljebb, ha több előadó számára közös: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +Ha olyan struktúrát használ, ahol az előadók egy könyvtárban vannak csoportosítva, a sablonok pedig a `templates` mappában találhatók, az elrendezés a következő helyeken várható: -Az elrendezés a következő fájlokban várható: +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` több előadónál közös elrendezés +Ha a bemutató egy modulban van, akkor a modul beágyazottságának megfelelően a könyvtárfán feljebb is keresni fog. -`` az aktuális előadó neve és `` az elrendezés neve, amely alapértelmezés szerint `'layout'`. A név megváltoztatható a `$this->setLayout('otherLayout')` segítségével, így a `@otherLayout.latte` fájlokat próbálja meg. +Az elrendezés nevét a `$this->setLayout('layoutAdmin')` segítségével lehet megváltoztatni, majd a `@layoutAdmin.latte` fájlban várjuk el. Az elrendezés sablonfájlt közvetlenül is megadhatja a `$this->setLayout('/path/to/template.latte')` segítségével. -Az elrendezéssablon fájlnevét közvetlenül is megadhatja a `$this->setLayout('/path/to/template.latte')` segítségével. A `$this->setLayout(false)` használata letiltja az elrendezés keresését. +A `$this->setLayout(false)` vagy a `{layout none}` címke használata a sablonon belül kikapcsolja az elrendezéskeresést. .[note] -A [formatLayoutTemplateFiles |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] metódus felülbírálásával módosíthatja a sablonok keresési útvonalait, amely a lehetséges fájlútvonalak tömbjét adja vissza. +A fájlok, amelyekben az elrendezési sablonok keresése történik, megváltoztathatók a [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] metódus felülbírálásával, amely a lehetséges fájlnevek tömbjét adja vissza. Változók a sablonban .[#toc-variables-in-the-template] @@ -104,7 +150,7 @@ A `@property-read` annotáció az IDE és a statikus elemzés számára készül A sablonokban is megengedheted magadnak a suttogás luxusát, csak telepítsd a Latte plugint a PhpStormban, és add meg az osztály nevét a sablon elején, lásd a "Latte: hogyan kell tipizálni a rendszert":https://blog.nette.org/hu/latte-hogyan-kell-hasznalni-a-tipusrendszert című cikket: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void A Latte 3. verziója egy fejlettebb módszert kínál, amely minden egyes webes projekthez egy [bővítményt |latte:creating-extension] hoz létre. Íme egy durva példa egy ilyen osztályra: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ A [configuration |configuration#Latte] segítségével regisztráljuk: ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ Alternatívaként a fordítót a [konfiguráció |configuration#Latte] segítsé ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` A fordító ekkor például a `|translate` szűrőként használható, a `translate()` metódusnak átadott további paraméterekkel (lásd `foo, bar`): diff --git a/application/it/@home.texy b/application/it/@home.texy index 6c54a56fdd..4ac0541e47 100644 --- a/application/it/@home.texy +++ b/application/it/@home.texy @@ -2,35 +2,84 @@ Applicazione Nette ****************** .[perex] -Il pacchetto `nette/application` è la base per la creazione di applicazioni web interattive. +Nette Application è il nucleo del framework Nette, che offre potenti strumenti per la creazione di moderne applicazioni web. Offre numerose caratteristiche eccezionali che semplificano notevolmente lo sviluppo e migliorano la sicurezza e la manutenibilità del codice. -- [Come funzionano le applicazioni? |how-it-works] -- [Bootstrap |Bootstrap] -- [Presentatori |Presenters] -- [Modelli |Templates] -- [Moduli |Modules] -- [Instradamento |Routing] -- [Creazione di collegamenti URL |creating-links] -- [Componenti interattivi |components] -- [AJAX e Snippet |ajax] -- [Moltiplicatore |multiplier] -- [Configurazione |Configuration] +Installazione .[#toc-installation] +---------------------------------- -Installazione -------------- - -Scaricare e installare il pacchetto utilizzando [Composer |best-practices:composer]: +Scaricare e installare la libreria con [Composer |best-practices:composer]: ```shell composer require nette/application ``` -| versione | compatibile con PHP + +Perché scegliere l'applicazione Nette? .[#toc-why-choose-nette-application] +--------------------------------------------------------------------------- + +Nette è da sempre un pioniere delle tecnologie web. + +**Router bidirezionale:** Nette dispone di un sistema di routing avanzato, unico nel suo genere per la sua bidirezionalità: non solo traduce gli URL in azioni dell'applicazione, ma può anche generare URL al contrario. Ciò significa che: +- È possibile modificare la struttura degli URL dell'intera applicazione in qualsiasi momento, senza modificare i file di template. +- Gli URL vengono canonicalizzati automaticamente, migliorando il SEO +- L'instradamento è definito in un unico punto, non disperso nelle annotazioni + +**Il sistema di componenti integrato, ispirato a Delphi e React.js, è unico tra i framework PHP: +- Permette di creare elementi riutilizzabili dell'interfaccia utente +- Supporta la composizione gerarchica dei componenti +- Offre un'elegante gestione delle richieste AJAX tramite segnali +- Ricca libreria di componenti già pronti su [Componette](https://componette.org). + +**AJAX e Snippet:** Nette ha introdotto un modo rivoluzionario di lavorare con AJAX nel 2009, prima di soluzioni come Hotwire per Ruby on Rails o Symfony UX Turbo: +- Gli snippet consentono di aggiornare solo alcune parti della pagina senza scrivere JavaScript. +- Integrazione automatica con il sistema di componenti +- Invalidazione intelligente delle sezioni della pagina +- Trasferimento minimo di dati + +**Il sistema di [template |latte:] più sicuro per PHP con funzionalità avanzate: +- Protezione XSS automatica con escape sensibile al contesto +- Estensibile con filtri, funzioni e tag personalizzati +- Ereditarietà dei template e snippet per AJAX +- Eccellente supporto di PHP 8.x con sistema di tipi + +**Iniezione delle dipendenze:** Nette utilizza pienamente l'iniezione delle dipendenze: +- Passaggio automatico delle dipendenze (autowiring) +- Configurazione tramite il chiaro formato NEON +- Supporto per le fabbriche di componenti + + +Vantaggi principali .[#toc-main-benefits] +----------------------------------------- + +- **Sicurezza**: Protezione automatica contro [vulnerabilità |nette:vulnerability-protection] come XSS, CSRF, ecc. +- **Produttività**: Meno scrittura, più funzioni grazie a un design intelligente +- Debug**: [Debugger Tracy |tracy:] con pannello di routing +- **Performance**: Sistema di caching intelligente, caricamento pigro dei componenti +- **Flessibilità**: Facile modifica dell'URL anche dopo il completamento dell'applicazione +- Componenti**: Sistema unico di elementi UI riutilizzabili +- Moderno**: Pieno supporto per PHP 8.4+ e per il sistema di tipi + + +Per iniziare .[#toc-getting-started] +------------------------------------ + +1. [Comprendere le applicazioni |how-it-works] - Comprendere l'architettura di base +2. [Presentatori |presenters] - Lavorare con i presentatori e le azioni +3. [Modelli |templates] - Creare modelli in Latte +4. [Instradamento |routing] - Configurazione degli URL +5. [Componenti interattivi |components] - Utilizzo del sistema di componenti + + +Compatibilità con PHP .[#toc-php-compatibility] +----------------------------------------------- + +| Versione compatibile con PHP |-----------|------------------- -| Applicazione Nette 4.0 | PHP 8.0 - 8.2 -| Applicazione Nette 3.1 | PHP 7.2 - 8.2 +| Applicazione Nette 4.0 | PHP 8.1 - 8.4 +| Applicazione Nette 3.2 | PHP 8.1 - 8.4 +| Applicazione Nette 3.1 | PHP 7.2 - 8.3 | Applicazione Nette 3.0 | PHP 7.1 - 8.0 | Applicazione Nette 2.4 | PHP 5.6 - 8.0 -Si applica alle ultime versioni della patch. +Valido per le ultime versioni della patch. \ No newline at end of file diff --git a/application/it/@left-menu.texy b/application/it/@left-menu.texy index 7fea73f2da..34c3de8c17 100644 --- a/application/it/@left-menu.texy +++ b/application/it/@left-menu.texy @@ -4,7 +4,7 @@ Applicazione Nette - [Bootstrap |Bootstrap] - [Presentatori |Presenters] - [Modelli |Templates] -- [Moduli |Modules] +- [Struttura della directory |directory-structure] - [Instradamento |Routing] - [Creazione di collegamenti URL |creating-links] - [Componenti interattivi |components] diff --git a/application/it/ajax.texy b/application/it/ajax.texy index 61d7bcd9ca..f15f19b518 100644 --- a/application/it/ajax.texy +++ b/application/it/ajax.texy @@ -3,10 +3,10 @@ AJAX e Snippet
    -Le moderne applicazioni web oggi vengono eseguite per metà su un server e per metà in un browser. AJAX è un fattore di unione vitale. Quale supporto offre il Nette Framework? -- invio di frammenti di template (i cosiddetti *snippet*) +Nell'era delle moderne applicazioni web, in cui le funzionalità si estendono spesso tra il server e il browser, AJAX è un elemento di connessione essenziale. Quali opzioni offre Nette Framework in questo ambito? +- invio di parti del modello, i cosiddetti snippet - passaggio di variabili tra PHP e JavaScript -- debug delle applicazioni AJAX +- strumenti per il debug delle richieste AJAX
    @@ -14,29 +14,32 @@ Le moderne applicazioni web oggi vengono eseguite per metà su un server e per m Richiesta AJAX .[#toc-ajax-request] =================================== -Una richiesta AJAX non differisce da una richiesta classica: il presentatore viene chiamato con una vista e dei parametri specifici. Il presentatore può anche decidere come rispondere: può usare la propria routine, che restituisce un frammento di codice HTML (snippet HTML), un documento XML, un oggetto JSON o codice JavaScript. +Una richiesta AJAX fondamentalmente non differisce da una richiesta HTTP classica. Un presentatore viene chiamato con parametri specifici. Spetta al presentatore rispondere alla richiesta: può restituire dati in formato JSON, inviare una parte di codice HTML, un documento XML, ecc. -Sul lato server, una richiesta AJAX può essere rilevata utilizzando il metodo di servizio [che incapsula la richiesta HTTP |http:request] `$httpRequest->isAjax()` (rileva in base all'intestazione HTTP `X-Requested-With`). All'interno del presentatore, è disponibile una scorciatoia sotto forma del metodo `$this->isAjax()`. +Dal lato del browser, si avvia una richiesta AJAX utilizzando la funzione `fetch()`: -Esiste un oggetto pre-elaborato chiamato `payload`, dedicato all'invio di dati al browser in JSON. - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // elaborazione della risposta +}); ``` -Per un controllo completo sull'output JSON, utilizzare il metodo `sendJson` nel presenter. Il metodo termina immediatamente il presentatore e si può fare a meno di un template: +Sul lato server, una richiesta AJAX viene riconosciuta dal metodo `$httpRequest->isAjax()` del servizio [che incapsula la richiesta HTTP |http:request]. Utilizza l'intestazione HTTP `X-Requested-With`, quindi è essenziale inviarla. All'interno del presenter, è possibile utilizzare il metodo `$this->isAjax()`. + +Se si desidera inviare dati in formato JSON, utilizzare il metodo [`sendJson()` |presenters#Sending a response] . Il metodo termina anche l'attività del presentatore. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Se vogliamo inviare HTML, possiamo impostare un modello speciale per le richieste AJAX: +Se si intende rispondere con un modello speciale progettato per AJAX, si può procedere come segue: ```php public function handleClick($param): void @@ -44,27 +47,43 @@ public function handleClick($param): void if ($this->isAjax()) { $this->template->setFile('path/to/ajax.latte'); } - // ... + //... } ``` +Frammenti .[#toc-snippets] +========================== + +Lo strumento più potente offerto da Nette per collegare il server al client sono gli snippet. Con essi, è possibile trasformare un'applicazione ordinaria in una AJAX con il minimo sforzo e poche righe di codice. L'esempio di Fifteen ne dimostra il funzionamento e il suo codice è disponibile su [GitHub |https://github.com/nette-examples/fifteen]. + +Gli snippet, o ritagli, consentono di aggiornare solo alcune parti della pagina, invece di ricaricare l'intera pagina. Questo è più veloce ed efficiente e offre anche un'esperienza d'uso più confortevole. Gli snippet potrebbero ricordare Hotwire per Ruby on Rails o Symfony UX Turbo. È interessante notare che Nette ha introdotto gli snippet 14 anni prima. + +Come funzionano gli snippet? Quando la pagina viene caricata per la prima volta (una richiesta non-AJAX), viene caricata l'intera pagina, compresi tutti gli snippet. Quando l'utente interagisce con la pagina (ad esempio, fa clic su un pulsante, invia un modulo, ecc.), invece di caricare l'intera pagina, viene effettuata una richiesta AJAX. Il codice del presentatore esegue l'azione e decide quali snippet devono essere aggiornati. Nette esegue il rendering di questi frammenti e li invia sotto forma di array JSON. Il codice di gestione del browser inserisce quindi gli snippet ricevuti nella pagina. Pertanto, viene trasferito solo il codice degli snippet modificati, risparmiando larghezza di banda e velocizzando il caricamento rispetto al trasferimento dell'intero contenuto della pagina. + + Naja .[#toc-naja] -================= +----------------- -La [libreria Naja |https://naja.js.org] è usata per gestire le richieste AJAX sul lato browser. [Installarla |https://naja.js.org/#/guide/01-install-setup-naja] come pacchetto node.js (da usare con Webpack, Rollup, Vite, Parcel e altri): +Per gestire gli snippet sul lato browser, si usa la [libreria Naja |https://naja.js.org]. [Installarla |https://naja.js.org/#/guide/01-install-setup-naja] come pacchetto node.js (da usare con applicazioni come Webpack, Rollup, Vite, Parcel e altre): ```shell npm install naja ``` -... o inserirla direttamente nel modello della pagina: +... o inserirla direttamente nel modello di pagina: ```html ``` -Per creare una richiesta AJAX da un normale link (segnale) o dall'invio di un modulo, è sufficiente contrassegnare il relativo link, modulo o pulsante con la classe `ajax`: +Per prima cosa è necessario [inizializzare |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] la libreria: + +```js +naja.initialize(); +``` + +Per rendere un normale link (segnale) o l'invio di un modulo una richiesta AJAX, è sufficiente contrassegnare il rispettivo link, modulo o pulsante con la classe `ajax`: ```html Go @@ -74,64 +93,39 @@ Per creare una richiesta AJAX da un normale link (segnale) o dall'invio di un mo or +
    ``` -Snippet .[#toc-snippets] -======================== - -Esiste uno strumento molto più potente del supporto AJAX integrato: gli snippet. Il loro utilizzo consente di trasformare una normale applicazione in un'applicazione AJAX utilizzando solo poche righe di codice. Il funzionamento è dimostrato nell'esempio di Fifteen, il cui codice è accessibile nella build o su [GitHub |https://github.com/nette-examples/fifteen]. - -Il modo in cui funzionano gli snippet è che l'intera pagina viene trasferita durante la richiesta iniziale (cioè non AJAX) e poi a ogni [sotto-richiesta |components#signal] AJAX (richiesta della stessa vista dello stesso presentatore) viene trasferito solo il codice delle parti modificate nel repository `payload` menzionato in precedenza. - -Gli snippet possono ricordare Hotwire per Ruby on Rails o Symfony UX Turbo, ma Nette li ha ideati quattordici anni prima. - +Ridisegno degli snippet .[#toc-redrawing-snippets] +-------------------------------------------------- -Invalidazione degli Snippet .[#toc-invalidation-of-snippets] -============================================================ - -Ogni discendente della classe [Control |components] (di cui fa parte anche un Presentatore) è in grado di ricordare se durante una richiesta sono state apportate modifiche che richiedono un nuovo rendering. Esistono due metodi per gestire questo aspetto: `redrawControl()` e `isControlInvalid()`. Un esempio: +Ogni oggetto della classe [Control |components] (compreso il Presentatore stesso) registra se sono avvenuti cambiamenti che richiedono il suo ridisegno. A tale scopo viene utilizzato il metodo `redrawControl()`. ```php public function handleLogin(string $user): void { - // L'oggetto deve essere ri-renderizzato dopo che l'utente ha effettuato il login + // dopo l'accesso, è necessario ridisegnare la parte pertinente $this->redrawControl(); - // ... + //... } ``` -Nette offre tuttavia una risoluzione ancora più fine rispetto ai componenti interi. I metodi elencati accettano il nome di un cosiddetto "snippet" come parametro opzionale. Uno "snippet" è fondamentalmente un elemento del modello contrassegnato a tale scopo da una tag di Latte, di cui si parlerà più avanti. In questo modo è possibile chiedere a un componente di ridisegnare solo *parti* del suo modello. Se l'intero componente viene invalidato, tutti i suoi frammenti vengono ridisegnati. Un componente è "invalido" anche se uno qualsiasi dei suoi sottocomponenti è invalido. - -```php -$this->isControlInvalid(); // -> false -$this->redrawControl('header'); // invalida lo snippet denominato 'header' -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, almeno uno snippet non è valido +Nette consente anche un controllo più preciso di ciò che deve essere ridisegnato. Il metodo summenzionato può accettare il nome dello snippet come argomento. In questo modo, è possibile invalidare (cioè forzare un ridisegno) a livello di parte del modello. Se l'intero componente viene invalidato, anche ogni frammento viene ridisegnato: -$this->redrawControl(); // invalida l'intero componente, ogni snippet -$this->isControlInvalid('footer'); // -> true +```php +// invalida lo snippet "header +$this->redrawControl('header'); ``` -Un componente che riceve un segnale viene automaticamente contrassegnato per essere ridisegnato. - -Grazie allo snippet redrawing sappiamo esattamente quali parti di quali elementi devono essere ridisegnate. - - -Tag `{snippet} … {/snippet}` .{toc: Tag snippet} -================================================ - -Il rendering della pagina procede in modo molto simile a una normale richiesta: vengono caricati gli stessi template, ecc. La parte fondamentale è, tuttavia, lasciare fuori le parti che non devono raggiungere l'output; le altre parti devono essere associate a un identificatore e inviate all'utente in un formato comprensibile per un gestore JavaScript. - -Sintassi .[#toc-syntax] ------------------------ +Frammenti in Latte .[#toc-snippets-in-latte] +-------------------------------------------- -Se c'è un controllo o uno snippet nel template, dobbiamo avvolgerlo usando il tag di coppia `{snippet} ... {/snippet}` - che farà in modo che lo snippet reso venga "tagliato" e inviato al browser. Lo racchiuderà anche in un tag di aiuto `
    ` (è possibile utilizzarne uno diverso). Nell'esempio seguente viene definito uno snippet chiamato `header`. Può anche rappresentare il modello di un componente: +L'uso degli snippet in Latte è estremamente semplice. Per definire una parte del modello come snippet, è sufficiente avvolgerla nei tag `{snippet}` e `{/snippet}`: ```latte {snippet header} @@ -139,7 +133,9 @@ Se c'è un controllo o uno snippet nel template, dobbiamo avvolgerlo usando il t {/snippet} ``` -Uno snippet di tipo diverso da `
    ` o uno snippet con attributi HTML aggiuntivi si ottiene utilizzando la variante dell'attributo: +Lo snippet crea un elemento `
    ` nella pagina HTML con un elemento `id` appositamente generato. Quando si ridisegna uno snippet, il contenuto di questo elemento viene aggiornato. Pertanto, quando la pagina viene inizialmente resa, devono essere resi anche tutti gli snippet, anche se inizialmente possono essere vuoti. + +È anche possibile creare uno snippet con un elemento diverso da `
    ` utilizzando l'attributo n:n: ```latte
    @@ -148,138 +144,106 @@ Uno snippet di tipo diverso da `
    ` o uno snippet con attributi HTML aggiunti ``` -Snippet dinamici .[#toc-dynamic-snippets] -========================================= +Aree Snippet .[#toc-snippet-areas] +---------------------------------- -In Nette è possibile definire snippet con un nome dinamico basato su un parametro di runtime. Ciò è particolarmente indicato per vari elenchi in cui è necessario modificare una sola riga, ma non si vuole trasferire l'intero elenco. Un esempio potrebbe essere: +I nomi dei frammenti possono anche essere espressioni: ```latte -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Esiste uno snippet statico chiamato `itemsContainer`, che contiene diversi snippet dinamici: `item-0`, `item-1` e così via. +In questo modo si otterranno diversi snippet come `item-0`, `item-1`, ecc. Se si invalidasse direttamente uno snippet dinamico (ad esempio, `item-1`), non verrebbe ridisegnato nulla. Il motivo è che gli snippet funzionano come veri e propri estratti e solo essi vengono resi direttamente. Tuttavia, nel template non esiste tecnicamente uno snippet chiamato `item-1`. Emerge solo quando si esegue il codice circostante lo snippet, in questo caso il ciclo foreach. Pertanto, contrassegneremo la parte del template che deve essere eseguita con il tag `{snippetArea}`: -Non è possibile ridisegnare direttamente uno snippet dinamico (il ridisegno di `item-1` non ha alcun effetto), ma è necessario ridisegnare il suo snippet padre (in questo esempio `itemsContainer`). Questo provoca l'esecuzione del codice dello snippet padre, ma poi solo i suoi sotto-snippet vengono inviati al browser. Se si vuole inviare solo uno dei sotto-nippet, è necessario modificare l'input dello snippet padre per non generare gli altri sotto-nippet. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -Nell'esempio precedente, bisogna assicurarsi che per una richiesta AJAX venga aggiunto un solo elemento all'array `$list`, quindi il ciclo `foreach` stamperà un solo frammento dinamico. +E ridisegneremo sia il singolo snippet che l'intera area circostante: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * This method returns data for the list. - * Usually this would just request the data from a model. - * For the purpose of this example, the data is hard-coded. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +È inoltre essenziale assicurarsi che l'array `$items` contenga solo gli elementi che devono essere ridisegnati. -Snippet in un template incluso .[#toc-snippets-in-an-included-template] -======================================================================= - -Può accadere che lo snippet si trovi in un template che viene incluso da un template diverso. In questo caso, occorre avvolgere il codice di inclusione nel secondo template con la tag `snippetArea`, quindi ridisegnare sia la snippetArea che lo snippet vero e proprio. - -La tag `snippetArea` assicura che il codice all'interno venga eseguito, ma che al browser venga inviato solo lo snippet effettivo nel modello incluso. +Quando si inserisce un altro modello in quello principale usando il tag `{include}`, che ha degli snippet, è necessario avvolgere nuovamente il modello incluso in un `snippetArea` e invalidare sia lo snippet che l'area insieme: ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* incluso.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -Si può anche combinare con gli snippet dinamici. +Snippet nei componenti .[#toc-snippets-in-components] +----------------------------------------------------- -Aggiunta e cancellazione .[#toc-adding-and-deleting] -==================================================== - -Se si aggiunge un nuovo elemento all'elenco e si invalida `itemsContainer`, la richiesta AJAX restituisce gli snippet che includono il nuovo elemento, ma il gestore javascript non sarà in grado di renderlo. Questo perché non esiste un elemento HTML con l'ID appena creato. - -In questo caso, il modo più semplice è avvolgere l'intero elenco in un altro frammento e invalidare il tutto: +È possibile creare snippet all'interno dei [componenti |components] e Nette li ridisegna automaticamente. Tuttavia, c'è una limitazione specifica: per ridisegnare gli snippet, viene chiamato il metodo `render()` senza alcun parametro. Pertanto, il passaggio di parametri nel modello non funziona: ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Invio di dati utente .[#toc-sending-user-data] +---------------------------------------------- + +Oltre agli snippet, è possibile inviare qualsiasi altro dato al client. È sufficiente scriverli nell'oggetto `payload`: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + //... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -Lo stesso vale per la cancellazione di un elemento. Sarebbe possibile inviare uno snippet vuoto, ma di solito gli elenchi possono essere paginati e sarebbe complicato implementare la cancellazione di un elemento e il caricamento di un altro (che si trovava in una pagina diversa dell'elenco paginato). - -Invio di parametri al componente .[#toc-sending-parameters-to-component] -======================================================================== +Parametri di invio .[#toc-sending-parameters] +============================================= Quando si inviano parametri al componente tramite una richiesta AJAX, sia che si tratti di parametri di segnale che di parametri persistenti, occorre fornire il loro nome globale, che contiene anche il nome del componente. Il nome completo del parametro restituisce il metodo `getParameterId()`. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -E gestire il metodo con i parametri corrispondenti nel componente. +Un metodo handle con i parametri corrispondenti nel componente: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/it/bootstrap.texy b/application/it/bootstrap.texy index 4efbe28af1..5c163445f0 100644 --- a/application/it/bootstrap.texy +++ b/application/it/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + private Configurator $configurator; + private string $rootDir; + + public function __construct() + { + $this->rootDir = dirname(__DIR__); + // Il configuratore è responsabile dell'impostazione dell'ambiente applicativo e dei servizi. + $this->configurator = new Configurator; + // Impostare la directory per i file temporanei generati da Nette (ad esempio, i modelli compilati). + $this->configurator->setTempDirectory($this->rootDir . '/temp'); + } + + public function bootWebApplication(): Nette\DI\Container + { + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); + } + + private function initializeEnvironment(): void { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // Nette è intelligente e la modalità di sviluppo si attiva automaticamente, + // oppure si può attivare per un indirizzo IP specifico decommentando la seguente riga: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Abilita Tracy: lo strumento di debug per eccellenza, il "coltellino svizzero". + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: carica automaticamente tutte le classi nella cartella data + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Carica i file di configurazione + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php .[#toc-index-php] =========================== -Nel caso delle applicazioni web, il file iniziale è `index.php`, che si trova nella cartella pubblica `www/`. Permette alla classe `Bootstrap` di inizializzare l'ambiente e restituisce la classe `$configurator` che crea il contenitore DI. Quindi ottiene il servizio `Application`, che esegue l'applicazione web: +Nel caso delle applicazioni web, il file principale è `index.php`, che si trova nella [cartella pubblica |directory-structure#public-directory-www] `www/`. La classe Bootstrap inizializzerà l'ambiente e produrrà un contenitore DI. Da qui ottiene il servizio `Application`, che avvia l'applicazione web: ```php -// inizializzare l'ambiente + ottenere l'oggetto Configuratore -$configurator = App\Bootstrap::boot(); -// creare un contenitore DI -$container = $configurator->createContainer(); -// Il contenitore DI crea un oggetto Nette\Application\Application +$bootstrap = new App\Bootstrap; +// Inizializzare l'ambiente + creare un contenitore DI +$container = $bootstrap->bootWebApplication(); +// Il contenitore DI crea un oggetto NetteApplicationApplication $application = $container->getByType(Nette\Application\Application::class); -// avvia l'applicazione Nette +// Avviare l'applicazione Nette e gestire la richiesta in arrivo $application->run(); ``` @@ -59,26 +84,42 @@ Come si può notare, la classe [api:Nette\Bootstrap\Configurator], che ora prese Modalità di sviluppo e modalità di produzione .[#toc-development-vs-production-mode] ==================================================================================== -Nette distingue due modalità di base per l'esecuzione di una richiesta: sviluppo e produzione. La modalità di sviluppo è incentrata sul massimo comfort del programmatore, Tracy viene visualizzato, la cache viene aggiornata automaticamente quando si modificano i template o la configurazione del contenitore DI, ecc. La modalità di produzione è incentrata sulle prestazioni, Tracy registra solo gli errori e le modifiche dei modelli e di altri file non vengono controllate. +Nette si comporta in modo diverso a seconda che venga eseguito su un server di sviluppo o di produzione: + +🛠️ Modalità di sviluppo: + - Visualizza la barra di debug Tracy con informazioni utili (ad esempio, query SQL, tempo di esecuzione, utilizzo della memoria). + - Mostra una pagina di errore dettagliata con le tracce delle chiamate di funzione e il contenuto delle variabili quando si verifica un errore. + - Aggiorna automaticamente la cache quando vengono modificati i modelli di Latte, i file di configurazione, ecc. + + +🚀 Modalità di produzione: + - Non visualizza alcuna informazione di debug; tutti gli errori vengono registrati. + - Mostra un `ErrorPresenter` o una pagina generica "Server Error" quando si verifica un errore. + - La cache non viene mai aggiornata automaticamente! + - Ottimizzato per la velocità e la sicurezza. -La selezione della modalità avviene tramite il rilevamento automatico, quindi di solito non è necessario configurare o cambiare qualcosa manualmente. La modalità è di sviluppo se l'applicazione è in esecuzione su localhost (cioè l'indirizzo IP `127.0.0.1` o `::1`) e non è presente alcun proxy (cioè la sua intestazione HTTP). Altrimenti, viene eseguita in modalità di produzione. + +La modalità è determinata automaticamente, quindi nella maggior parte dei casi non è necessario configurarla o cambiarla manualmente: + +- Modalità di sviluppo: Attivo su localhost (indirizzo IP `127.0.0.1` o `::1`) a meno che non sia in uso un proxy (ad esempio, in base alle intestazioni HTTP). +- Modalità di produzione: Attivo ovunque. Se si vuole abilitare la modalità di sviluppo in altri casi, ad esempio per i programmatori che accedono da un indirizzo IP specifico, si può usare `setDebugMode()`: ```php -$configurator->setDebugMode('23.75.345.200'); // uno o più indirizzi IP +$this->configurator->setDebugMode('23.75.345.200'); // uno o più indirizzi IP ``` Consigliamo assolutamente di combinare un indirizzo IP con un cookie. Nel cookie `nette-debug` verrà memorizzato un token segreto, ad esempio `secret1234`, e la modalità di sviluppo verrà attivata per i programmatori con questa combinazione di IP e cookie. ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` Possiamo anche disattivare completamente la modalità sviluppatore, anche per localhost: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` Si noti che il valore `true` attiva la modalità sviluppatore, cosa che non dovrebbe mai accadere su un server di produzione. @@ -90,7 +131,7 @@ Strumento di debug Tracy .[#toc-debugging-tool-tracy] Per facilitare il debug, attiviamo l'ottimo strumento [Tracy |tracy:]. In modalità sviluppatore visualizza gli errori e in modalità produzione li registra nella directory specificata: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ File temporanei .[#toc-temporary-files] Nette utilizza la cache per il contenitore DI, il RobotLoader, i modelli, ecc. Per questo motivo è necessario impostare il percorso della cartella in cui verrà memorizzata la cache: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` Su Linux o macOS, impostare i [permessi di scrittura |nette:troubleshooting#Setting directory permissions] per le directory `log/` e `temp/`. @@ -112,7 +153,7 @@ RobotLoader .[#toc-robotloader] Di solito, vogliamo caricare automaticamente le classi usando [RobotLoader |robot-loader:], quindi dobbiamo avviarlo e fargli caricare le classi dalla directory in cui si trova `Bootstrap.php` (cioè `__DIR__`) e da tutte le sue sottodirectory: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ Fuso orario .[#toc-timezone] Il configuratore consente di specificare un fuso orario per l'applicazione. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ In modalità di sviluppo, il contenitore viene aggiornato automaticamente ogni v I file di configurazione vengono caricati usando `addConfig()`: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` Il metodo `addConfig()` può essere richiamato più volte per aggiungere più file. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ Parametri statici .[#toc-static-parameters] I parametri usati nei file di configurazione possono essere definiti [nella sezione `parameters` |dependency-injection:configuration#parameters] e anche passati (o sovrascritti) dal metodo `addStaticParameters()` (ha l'alias `addParameters()`). È importante che valori diversi dei parametri causino la generazione di contenitori DI aggiuntivi, cioè di classi aggiuntive. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ Parametri dinamici .[#toc-dynamic-parameters] Possiamo anche aggiungere parametri dinamici al contenitore; i loro diversi valori, a differenza dei parametri statici, non causeranno la generazione di nuovi contenitori DI. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ Le variabili d'ambiente possono essere facilmente rese disponibili usando parametri dinamici. Possiamo accedervi tramite `%env.variable%` nei file di configurazione. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ Parametri predefiniti .[#toc-default-parameters] - `%wwwDir%` è il percorso assoluto della directory contenente il file di ingresso `index.php` - `%tempDir%` è il percorso assoluto della directory per i file temporanei - `%vendorDir%` è il percorso assoluto della directory in cui Composer installa le librerie +- `%rootDir%` è il percorso assoluto della directory principale del progetto - `%debugMode%` indica se l'applicazione è in modalità debug - `%consoleMode%` indica se la richiesta è arrivata attraverso la riga di comando @@ -225,7 +268,7 @@ services: Creare una nuova istanza e inserirla in bootstrap: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Ambienti diversi .[#toc-different-environments] =============================================== -Sentitevi liberi di personalizzare la classe `Bootstrap` in base alle vostre esigenze. Si possono aggiungere parametri al metodo `boot()` per differenziare i progetti web, oppure aggiungere altri metodi, come `bootForTests()`, che inizializza l'ambiente per i test unitari, `bootForCli()` per gli script chiamati dalla riga di comando e così via. +Non esitate a personalizzare la classe `Bootstrap` in base alle vostre esigenze. Si possono aggiungere parametri al metodo `bootWebApplication()` per differenziare i progetti web. In alternativa, si possono aggiungere altri metodi, come `bootTestEnvironment()` per inizializzare l'ambiente per i test unitari, `bootConsoleApplication()` per gli script chiamati dalla riga di comando e così via. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container +{ + Tester\Environment::setup(); // Inizializzazione del tester Nette + $this->setupContainer(); + return $this->configurator->createContainer(); +} + +public function bootConsoleApplication(): Nette\DI\Container { - $configurator = self::boot(); - Tester\Environment::setup(); // Nette Tester initialization - return $configurator; + $this->configurator->setDebugMode(false); + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); } ``` diff --git a/application/it/components.texy b/application/it/components.texy index ad8d123bb0..a4198af736 100644 --- a/application/it/components.texy +++ b/application/it/components.texy @@ -230,6 +230,28 @@ Nel modello, questi messaggi sono disponibili nella variabile `$flashes` come og ``` +Reindirizzamento dopo un segnale .[#toc-redirection-after-a-signal] +=================================================================== + +Dopo l'elaborazione di un segnale di un componente, spesso segue un reindirizzamento. Questa situazione è simile a quella dei moduli: dopo l'invio di un modulo, si effettua un reindirizzamento per evitare che i dati vengano inviati nuovamente quando la pagina viene aggiornata nel browser. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Poiché un componente è un elemento riutilizzabile e di solito non dovrebbe avere una dipendenza diretta da presentatori specifici, i metodi `redirect()` e `link()` interpretano automaticamente il parametro come un segnale di componente: + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +Se è necessario reindirizzare a un presentatore o a un'azione diversa, lo si può fare attraverso il presentatore: + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Parametri persistenti .[#toc-persistent-parameters] =================================================== @@ -347,7 +369,7 @@ services: Infine, utilizzeremo questo factory nel nostro presenter: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ Componenti in profondità .[#toc-components-in-depth] I componenti di un'applicazione Nette sono le parti riutilizzabili di un'applicazione Web che vengono incorporate nelle pagine, argomento di questo capitolo. Quali sono esattamente le funzionalità di un componente? 1) è renderizzabile in un modello -2) sa quale parte di sé rendere durante una [richiesta AJAX |ajax#invalidation] (snippet) +2) sa [quale parte di se stesso |ajax#snippets] rendere durante una richiesta AJAX (snippet) 3) ha la capacità di memorizzare il proprio stato in un URL (parametri persistenti) 4) ha la capacità di rispondere alle azioni dell'utente (segnali) 5) crea una struttura gerarchica (dove la radice è il presenter) diff --git a/application/it/configuration.texy b/application/it/configuration.texy index 2fc47b2fab..20860ad839 100644 --- a/application/it/configuration.texy +++ b/application/it/configuration.texy @@ -13,11 +13,15 @@ application: # mostra il pannello "Applicazione Nette" in Tracy BlueScreen? debugger: ... # (bool) predefinito a true - # Il presentatore di errori sarà chiamato in caso di errore? - catchExceptions: ... # (bool) predefinito a true in modalità di produzione + # Il presentatore di errori sarà chiamato su un errore? + # ha effetto solo in modalità sviluppatore + catchExceptions: ... # (bool) predefinito a true # nome del presentatore di errori - errorPresenter: Error # (string) predefinito a 'Nette:Error'. + errorPresenter: Error # (string|array) predefinito a 'Nette:Error'. + + # definisce gli alias per i presentatori e gli eventi + aliases: ... # definisce le regole per risolvere il nome del presentatore in una classe mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) predefinito a false ``` -Poiché i presentatori di errori non vengono richiamati per impostazione predefinita in modalità di sviluppo e gli errori vengono visualizzati da Tracy, la modifica del valore `catchExceptions` a `true` aiuta a verificare che i presentatori di errori funzionino correttamente durante lo sviluppo. +Dalla versione 3.2 di `nette/application` è possibile definire una coppia di presentatori di errori: + +```neon +application: + errorPresenter: + 4xx: Error4xx # per NetteApplicationBadRequestException + 5xx: Error5xx # per altre eccezioni +``` L'opzione `silentLinks` determina il comportamento di Nette in modalità sviluppatore quando la generazione dei collegamenti fallisce (ad esempio, perché non c'è un presentatore, ecc.). Il valore predefinito `false` significa che Nette attiva `E_USER_WARNING`. L'impostazione di `true` sopprime questo messaggio di errore. In un ambiente di produzione, `E_USER_WARNING` viene sempre invocato. Si può anche influenzare questo comportamento impostando la variabile del presentatore [$invalidLinkMode |creating-links#Invalid Links]. -La [mappatura definisce le regole |modules#mapping] con cui il nome della classe viene derivato dal nome del presentatore. +Gli [pseudonimi semplificano il riferimento ai |creating-links#aliases] presentatori utilizzati di frequente. + +La [mappatura definisce le regole |directory-structure#Presenter Mapping] con cui il nome della classe viene derivato dal nome del presentatore. Registrazione automatica dei presentatori .[#toc-automatic-registration-of-presenters] @@ -80,7 +93,10 @@ latte: strictParsing: ... # (bool) l'impostazione predefinita è false # abilita il [controllo del codice generato |latte:develop#Checking Generated Code] - phpLinter: ... # (stringa) il valore predefinito è null + phpLinter: ... # (string) il valore predefinito è null + + # imposta il locale + locale: cs_CZ # (string) il valore predefinito è null # classe di $this->template templateClass: App\MyTemplateClass # predefinita a Nette\Bridges\ApplicationLatte\DefaultTemplate @@ -91,7 +107,7 @@ Se si utilizza la versione 3 di Latte, è possibile aggiungere nuove [estensioni ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` /--comment diff --git a/application/it/creating-links.texy b/application/it/creating-links.texy index 7b8cafdf00..33813aa31d 100644 --- a/application/it/creating-links.texy +++ b/application/it/creating-links.texy @@ -38,7 +38,7 @@ Cliccare su un link è, in parole povere, come chiamare un metodo `ProductPresen detail ``` -Se il metodo `ProductPresenter::renderShow()` non ha `$lang` nella sua firma, può leggere il valore del parametro usando `$lang = $this->getParameter('lang')`. +Se il metodo `ProductPresenter::renderShow()` non ha `$lang` nella sua firma, può recuperare il valore del parametro usando `$lang = $this->getParameter('lang')` o dalla [proprietà |presenters#Request Parameters]. Se i parametri sono memorizzati in un array, possono essere espansi con l'operatore `...` (o `(expand)` in Latte 2.x): @@ -103,7 +103,7 @@ Se l'azione è `default`, possiamo ometterlo, ma i due punti devono rimanere: home ``` -I collegamenti possono anche puntare ad altri [moduli |modules]. In questo caso, i collegamenti si distinguono in relativi ai sottomoduli o assoluti. Il principio è analogo a quello dei percorsi su disco, solo che al posto degli slash ci sono i punti. Supponiamo che il presentatore attuale faccia parte del modulo `Front`, quindi scriveremo: +I collegamenti possono anche puntare ad altri [moduli |directory-structure#Presenters and Templates]. In questo caso, i collegamenti si distinguono in relativi ai sottomoduli o assoluti. Il principio è analogo a quello dei percorsi su disco, solo che al posto degli slash ci sono i punti. Supponiamo che il presentatore attuale faccia parte del modulo `Front`, quindi scriveremo: ```latte link to Front:Shop:Product:show @@ -140,7 +140,7 @@ Il target `this` creerà un collegamento alla pagina corrente: refresh ``` -Allo stesso tempo, tutti i parametri specificati nella firma del metodo `render()` o `action()` vengono trasferiti. Quindi, se ci troviamo nelle pagine `Product:show` e `id:123`, anche il collegamento a `this` passerà questo parametro. +Allo stesso tempo, tutti i parametri specificati nella firma dell'elemento `action()` o `render()` se il metodo `action()` non è definito, vengono trasferiti. Quindi, se ci troviamo nelle pagine `Product:show` e `id:123`, il collegamento a `this` passerà anche questo parametro. Naturalmente, è possibile specificare direttamente i parametri: @@ -213,7 +213,7 @@ Poiché i [componenti |components] sono unità riutilizzabili separate che non d Se vogliamo collegarci ai presentatori nel modello del componente, usiamo il tag `{plink}`: ```latte -home +home ``` o nel codice @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Alias .[#toc-aliases]{data-version:v3.2.2} +========================================== + +A volte è utile assegnare un alias facilmente memorizzabile a una coppia Presentatore:azione. Per esempio, si può nominare la pagina iniziale `Front:Home:default` semplicemente come `home` o `Admin:Dashboard:default` come `admin`. + +Gli alias sono definiti nella [configurazione |configuration] sotto la chiave `application › aliases`: + +```neon +application: + aliases: + home: Front:Home:default + admin: Admin:Dashboard:default + sign: Front:Sign:in +``` + +Nei collegamenti, vengono scritti utilizzando il simbolo at, ad esempio: + +```latte +administration +``` + +Sono supportati in tutti i metodi che lavorano con i collegamenti, come `redirect()` e simili. + + Collegamenti non validi .[#toc-invalid-links] ============================================= @@ -257,6 +281,6 @@ Come creare link con il metodo `link()`, ma senza la presenza di un presentatore LinkGenerator è un servizio che si può far passare attraverso il costruttore e poi creare link con il suo metodo `link()`. -C'è una differenza rispetto ai presentatori. LinkGenerator crea tutti i collegamenti come URL assoluti. Inoltre, non esiste un "presentatore corrente", quindi non è possibile specificare solo il nome dell'azione `link('default')` o i percorsi relativi ai [moduli |modules]. +Rispetto ai presentatori, c'è una differenza. LinkGenerator crea tutti i collegamenti direttamente come URL assoluti. Inoltre, non c'è un "presentatore vero e proprio", quindi non è possibile elencare il nome dell'azione `link('default')` come target o elencare percorsi relativi ai moduli. I collegamenti non validi lanciano sempre `Nette\Application\UI\InvalidLinkException`. diff --git a/application/it/directory-structure.texy b/application/it/directory-structure.texy new file mode 100644 index 0000000000..3d2d59b75d --- /dev/null +++ b/application/it/directory-structure.texy @@ -0,0 +1,526 @@ +Struttura delle directory dell'applicazione +******************************************* + +
    + +Come progettare una struttura di directory chiara e scalabile per i progetti in Nette Framework? Vi mostreremo pratiche collaudate che vi aiuteranno a organizzare il vostro codice. Imparerete: + +- come **strutturare logicamente** l'applicazione in directory +- come progettare la struttura per **scalare bene** con la crescita del progetto +- quali sono le **possibili alternative** e i loro vantaggi o svantaggi + +
    + + +È importante ricordare che Nette Framework non insiste su alcuna struttura specifica. È stato progettato per essere facilmente adattabile a qualsiasi esigenza e preferenza. + + +Struttura di base del progetto .[#toc-basic-project-structure] +============================================================== + +Sebbene Nette Framework non imponga una struttura di directory fissa, esiste una disposizione predefinita e collaudata sotto forma di [Web Project |https://github.com/nette/web-project]: + +/--pre +web-project/ +├── app/ ← directory dell'applicazione +├── assets/ ← file SCSS, JS, immagini..., in alternativa resources/ +├── bin/ ← script della riga di comando +├── config/ ← configurazione +├── log/ ← errori registrati +├── temp/ ← file temporanei, cache +├── tests/ ← test +├── vendor/ ← librerie installate da Composer +└── www/ ← directory pubblica (document-root) +\-- + +È possibile modificare liberamente questa struttura in base alle proprie esigenze, rinominando o spostando le cartelle. È sufficiente modificare i percorsi relativi alle cartelle in `Bootstrap.php` ed eventualmente in `composer.json`. Non serve nient'altro, nessuna riconfigurazione complessa, nessuna modifica costante. Nette ha un rilevamento automatico intelligente e riconosce automaticamente la posizione dell'applicazione, compresa la sua base URL. + + +Principi di organizzazione del codice .[#toc-code-organization-principles] +========================================================================== + +Quando si esplora per la prima volta un nuovo progetto, si dovrebbe essere in grado di orientarsi rapidamente. Immaginate di fare clic sulla cartella `app/Model/` e di vedere questa struttura: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +Da qui si apprende solo che il progetto utilizza alcuni servizi, repository ed entità. Non si apprende nulla sullo scopo effettivo dell'applicazione. + +Vediamo un approccio diverso: **organizzazione per domini**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Questo è diverso: a prima vista è chiaro che si tratta di un sito di e-commerce. I nomi stessi delle directory rivelano ciò che l'applicazione può fare: lavora con pagamenti, ordini e prodotti. + +Il primo approccio (organizzazione per tipo di classe) comporta diversi problemi nella pratica: il codice che è logicamente correlato è sparso in diverse cartelle e bisogna saltare da una all'altra. Pertanto, organizzeremo per domini. + + +Spazi dei nomi .[#toc-namespaces] +--------------------------------- + +È convenzionale che la struttura delle directory corrisponda agli spazi dei nomi nell'applicazione. Ciò significa che la posizione fisica dei file corrisponde al loro spazio dei nomi. Per esempio, una classe situata in `app/Model/Product/ProductRepository.php` dovrebbe avere lo spazio dei nomi `App\Model\Product`. Questo principio aiuta a orientare il codice e semplifica il caricamento automatico. + + +Singolare e plurale nei nomi .[#toc-singular-vs-plural-in-names] +---------------------------------------------------------------- + +Si noti che usiamo il singolare per le directory delle applicazioni principali: `app`, `config`, `log`, `temp`, `www`. Lo stesso vale all'interno dell'applicazione: `Model`, `Core`, `Presentation`. Questo perché ognuno di essi rappresenta un concetto unificato. + +Allo stesso modo, `app/Model/Product` rappresenta tutto ciò che riguarda i prodotti. Non lo chiamiamo `Products` perché non è una cartella piena di prodotti (che conterrebbe file come `iphone.php`, `samsung.php`). È uno spazio dei nomi che contiene classi per lavorare con i prodotti - `ProductRepository.php`, `ProductService.php`. + +La cartella `app/Tasks` è al plurale perché contiene un insieme di script eseguibili autonomi - `CleanupTask.php`, `ImportTask.php`. Ognuno di essi è un'unità indipendente. + +Per coerenza, si consiglia di usare: +- Singolare per gli spazi dei nomi che rappresentano un'unità funzionale (anche se si lavora con più entità). +- Plurale per le collezioni di unità indipendenti +- In caso di incertezza o se non si vuole pensarci, scegliere singolare + + +Elenco pubblico `www/` .[#toc-public-directory-www] +=================================================== + +Questa directory è l'unica accessibile dal web (la cosiddetta document-root). Spesso si può trovare il nome `public/` invece di `www/`: è solo una questione di convenzione e non influisce sulla funzionalità. La directory contiene: +- [Punto di ingresso |bootstrap#index.php] dell'applicazione `index.php` +- File `.htaccess` con regole di mod_rewrite (per Apache) +- File statici (CSS, JavaScript, immagini) +- File caricati + +Per una corretta sicurezza dell'applicazione, è fondamentale avere una [document-root configurata |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url] correttamente. + +.[note] +Non collocare mai la cartella `node_modules/` in questa directory: contiene migliaia di file che possono essere eseguibili e non dovrebbero essere accessibili al pubblico. + + +Directory delle applicazioni `app/` .[#toc-application-directory-app] +===================================================================== + +Questa è la directory principale con il codice dell'applicazione. Struttura di base: + +/--pre +app/ +├── Core/ ← L'infrastruttura è importante +├── Model/ ← logica aziendale +├── Presentation/ ← presentatori e modelli +├── Tasks/ ← script di comando +└── Bootstrap.php ← classe bootstrap dell'applicazione +\-- + +`Bootstrap.php` è la [classe di avvio dell'applicazione |bootstrap] che inizializza l'ambiente, carica la configurazione e crea il contenitore DI. + +Vediamo ora in dettaglio le singole sottodirectory. + + +Presentatori e modelli .[#toc-presenters-and-templates] +======================================================= + +La parte di presentazione dell'applicazione si trova nella directory `app/Presentation`. Un'alternativa è la breve `app/UI`. Qui si trovano tutti i presentatori, i loro modelli e tutte le classi di aiuto. + +Organizziamo questo livello per domini. In un progetto complesso, che combina e-commerce, blog e API, la struttura sarebbe la seguente: + +/--pre +app/Presentation/ +├── Shop/ ← frontend e-commerce +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← blog +│ ├── Home/ +│ └── Post/ +├── Admin/ ← amministrazione +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← endpoint API + └── V1/ +\-- + +Al contrario, per un semplice blog utilizzeremmo questa struttura: + +/--pre +app/Presentation/ +├── Front/ ← sito web frontend +│ ├── Home/ +│ └── Post/ +├── Admin/ ← amministrazione +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, sitemaps, ecc. +\-- + +Cartelle come `Home/` o `Dashboard/` contengono presentatori e modelli. Cartelle come `Front/`, `Admin/` o `Api/` sono chiamate **moduli**. Tecnicamente, si tratta di cartelle regolari che servono per l'organizzazione logica dell'applicazione. + +Ogni cartella con un presentatore contiene un presentatore con nome simile e i relativi modelli. Ad esempio, la cartella `Dashboard/` contiene: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← presentatore +└── default.latte ← modello +\-- + +Questa struttura di directory si riflette negli spazi dei nomi delle classi. Ad esempio, `DashboardPresenter` si trova nello spazio dei nomi `App\Presentation\Admin\Dashboard` (vedere la [mappatura dei presentatori |#presenter mapping]): + +```php +namespace App\Presentation\Admin\Dashboard; + +class DashboardPresenter extends Nette\Application\UI\Presenter +{ + //... +} +``` + +Ci riferiamo al presentatore `Dashboard` all'interno del modulo `Admin` nell'applicazione usando la notazione dei due punti come `Admin:Dashboard`. Alla sua azione `default` ci si riferisce quindi come `Admin:Dashboard:default`. Per i moduli annidati, usiamo più punti, ad esempio `Shop:Order:Detail:default`. + + +Sviluppo di una struttura flessibile .[#toc-flexible-structure-development] +--------------------------------------------------------------------------- + +Uno dei grandi vantaggi di questa struttura è l'eleganza con cui si adatta alle crescenti esigenze del progetto. A titolo di esempio, prendiamo la parte che genera i feed XML. Inizialmente, abbiamo un semplice modulo: + +/--pre +Export/ +├── ExportPresenter.php ← un unico presentatore per tutte le esportazioni +├── sitemap.latte ← modello per la mappa del sito +└── feed.latte ← modello per il feed RSS +\-- + +Nel corso del tempo, vengono aggiunti altri tipi di feed e abbiamo bisogno di più logica per loro... Nessun problema! La cartella `Export/` diventa semplicemente un modulo: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← feed per Amazon + └── ebay.latte ← feed per eBay +\-- + +Questa trasformazione è del tutto agevole: basta creare nuove sottocartelle, suddividervi il codice e aggiornare i collegamenti (ad esempio, da `Export:feed` a `Export:Feed:amazon`). Grazie a ciò, possiamo espandere gradualmente la struttura secondo le necessità, il livello di annidamento non è limitato in alcun modo. + +Ad esempio, se nell'amministrazione sono presenti molti presentatori relativi alla gestione degli ordini, come `OrderDetail`, `OrderEdit`, `OrderDispatch` ecc. si può creare un modulo (cartella) `Order` per una migliore organizzazione, che conterrà (cartelle per) i presentatori `Detail`, `Edit`, `Dispatch` e altri. + + +Posizione del modello .[#toc-template-location] +----------------------------------------------- + +Negli esempi precedenti, abbiamo visto che i modelli si trovano direttamente nella cartella del presentatore: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← presentatore +├── DashboardTemplate.php ← classe modello opzionale +└── default.latte ← modello +\-- + +Questa posizione si rivela la più comoda nella pratica: si hanno tutti i file correlati a portata di mano. + +In alternativa, è possibile collocare i modelli in una sottocartella di `templates/`. Nette supporta entrambe le varianti. È anche possibile collocare i modelli completamente al di fuori della cartella `Presentation/`. Per maggiori informazioni sulle opzioni di collocazione dei modelli, consultare il capitolo [Ricerca dei modelli |templates#Template Lookup]. + + +Classi e componenti di aiuto .[#toc-helper-classes-and-components] +------------------------------------------------------------------ + +I presentatori e i modelli sono spesso accompagnati da altri file di aiuto. Li collochiamo logicamente in base al loro scopo: + +1. **Direttamente con il presentatore** nel caso di componenti specifici per un determinato presentatore: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← componente per l'elenco dei prodotti +└── FilterForm.php ← modulo per il filtraggio +\-- + +2. **Per il modulo** - si consiglia di utilizzare la cartella `Accessory`, che si trova ordinatamente all'inizio dell'alfabeto: + +/--pre +Front/ +├── Accessory/ +│ ├── NavbarControl.php ← componenti per il frontend +│ └── TemplateFilters.php +├── Product/ +└── Cart/ +\-- + +3. **Per l'intera applicazione** - in `Presentation/Accessory/`: +/--pre +app/Presentation/ +├── Accessory/ +│ ├── LatteExtension.php +│ └── TemplateFilters.php +├── Front/ +└── Admin/ +\-- + +Oppure si possono inserire classi di aiuto come `LatteExtension.php` o `TemplateFilters.php` nella cartella dell'infrastruttura `app/Core/Latte/`. E i componenti in `app/Components`. La scelta dipende dalle convenzioni del team. + + +Modello - Cuore dell'applicazione .[#toc-model-heart-of-the-application] +======================================================================== + +Il modello contiene tutta la logica di business dell'applicazione. Per la sua organizzazione, vale la stessa regola: si struttura per domini: + +/--pre +app/Model/ +├── Payment/ ← tutto sui pagamenti +│ ├── PaymentFacade.php ← punto di ingresso principale +│ ├── PaymentRepository.php +│ ├── Payment.php ← entità +├── Order/ ← tutto sugli ordini +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← tutto sulla spedizione +\-- + +Nel modello si incontrano tipicamente questi tipi di classi: + +**Facades**: rappresentano il punto di ingresso principale in un dominio specifico dell'applicazione. Agiscono come un orchestratore che coordina la cooperazione tra diversi servizi per implementare casi d'uso completi (come "creare un ordine" o "elaborare un pagamento"). Sotto il loro livello di orchestrazione, la facciata nasconde i dettagli dell'implementazione al resto dell'applicazione, fornendo così un'interfaccia pulita per lavorare con il dominio in questione. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // convalida + // creazione dell'ordine + // invio di e-mail + // scrittura su statistiche + } +} +``` + +**Servizi**: si concentrano su operazioni commerciali specifiche all'interno di un dominio. A differenza delle facciate, che orchestrano interi casi d'uso, un servizio implementa una logica aziendale specifica (come il calcolo dei prezzi o l'elaborazione dei pagamenti). I servizi sono tipicamente stateless e possono essere utilizzati sia dalle facade come elementi costitutivi per operazioni più complesse, sia direttamente da altre parti dell'applicazione per compiti più semplici. + +```php +class PricingService +{ + public function calculateTotal(Order $order): Money + { + // calcolo del prezzo + } +} +``` + +**Repository**: gestiscono tutte le comunicazioni con l'archivio dati, in genere un database. Il loro compito è quello di caricare e salvare le entità e di implementare metodi per la loro ricerca. Un repository protegge il resto dell'applicazione dai dettagli dell'implementazione del database e fornisce un'interfaccia orientata agli oggetti per lavorare con i dati. + +```php +class OrderRepository +{ + public function find(int $id): ?Order + { + } + + public function findByCustomer(int $customerId): array + { + } +} +``` + +**Entità**: oggetti che rappresentano i principali concetti di business dell'applicazione, che hanno una loro identità e cambiano nel tempo. In genere si tratta di classi mappate sulle tabelle del database tramite ORM (come Nette Database Explorer o Doctrine). Le entità possono contenere regole di business relative ai loro dati e alla logica di validazione. + +```php +// Entità mappata alla tabella del database ordini +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, + ]); + } +} +``` + +**Oggetti valore**: oggetti immutabili che rappresentano valori senza una propria identità, ad esempio una somma di denaro o un indirizzo e-mail. Due istanze di un oggetto valore con gli stessi valori sono considerate identiche. + + +Codice dell'infrastruttura .[#toc-infrastructure-code] +====================================================== + +La cartella `Core/` (o anche `Infrastructure/`) ospita le fondamenta tecniche dell'applicazione. Il codice dell'infrastruttura include tipicamente: + +/--pre +app/Core/ +├── Router/ ← instradamento e gestione degli URL +│ └── RouterFactory.php +├── Security/ ← autenticazione e autorizzazione +│ ├── Authenticator.php +│ └── Authorizator.php +├── Logging/ ← registrazione e monitoraggio +│ ├── SentryLogger.php +│ └── FileLogger.php +├── Cache/ ← livello di caching +│ └── FullPageCache.php +└── Integration/ ← integrazione con servizi esterni + ├── Slack/ + └── Stripe/ +\-- + +Per i progetti più piccoli, una struttura piatta è naturalmente sufficiente: + +/--pre +Core/ +├── RouterFactory.php +├── Authenticator.php +└── QueueMailer.php +\-- + +Questo è codice che: + +- Gestisce l'infrastruttura tecnica (routing, logging, caching) +- integra servizi esterni (Sentry, Elasticsearch, Redis) +- Fornisce servizi di base per l'intera applicazione (posta, database) +- È per lo più indipendente dal dominio specifico: la cache o il logger funzionano allo stesso modo per l'e-commerce o il blog. + +Ci si chiede se una certa classe debba stare qui o nel modello? La differenza fondamentale è che il codice in `Core/`: + +- Non sa nulla del dominio (prodotti, ordini, articoli). +- Di solito può essere trasferito a un altro progetto +- Risolve "come funziona" (come inviare la posta), non "cosa fa" (quale posta inviare) + +Esempio per una migliore comprensione: + +- `App\Core\MailerFactory` - crea istanze di classe per l'invio di email, gestisce le impostazioni SMTP +- `App\Model\OrderMailer` - utilizza `MailerFactory` per inviare le e-mail sugli ordini, conosce i loro modelli e quando devono essere inviati + + +Script di comando .[#toc-command-scripts] +========================================= + +Le applicazioni hanno spesso bisogno di eseguire compiti al di fuori delle normali richieste HTTP, che si tratti di elaborazione di dati in background, manutenzione o compiti periodici. Per l'esecuzione vengono utilizzati semplici script nella cartella `bin/`, mentre la logica di implementazione vera e propria è collocata in `app/Tasks/` (o `app/Commands/`). + +Esempio: + +/--pre +app/Tasks/ +├── Maintenance/ ← script di manutenzione +│ ├── CleanupCommand.php ← eliminazione di vecchi dati +│ └── DbOptimizeCommand.php ← ottimizzazione del database +├── Integration/ ← integrazione con sistemi esterni +│ ├── ImportProducts.php ← importazione dal sistema dei fornitori +│ └── SyncOrders.php ← sincronizzazione degli ordini +└── Scheduled/ ← attività regolari + ├── NewsletterCommand.php ← invio di newsletter + └── ReminderCommand.php ← notifiche ai clienti +\-- + +Cosa appartiene al modello e cosa agli script di comando? Ad esempio, la logica per l'invio di un'e-mail fa parte del modello, l'invio massivo di migliaia di e-mail appartiene a `Tasks/`. + +I task vengono solitamente [eseguiti dalla riga di comando |https://blog.nette.org/en/cli-scripts-in-nette-application] o tramite cron. Possono anche essere eseguiti tramite richiesta HTTP, ma occorre tenere conto della sicurezza. Il presenter che esegue il task deve essere protetto, ad esempio solo per gli utenti loggati o con un token forte e l'accesso da indirizzi IP consentiti. Per i task lunghi, è necessario aumentare il limite di tempo dello script e utilizzare `session_write_close()` per evitare di bloccare la sessione. + + +Altre possibili directory .[#toc-other-possible-directories] +============================================================ + +Oltre alle directory di base menzionate, è possibile aggiungere altre cartelle specializzate in base alle esigenze del progetto. Vediamo le più comuni e il loro utilizzo: + +/--pre +app/ +├── Api/ ← Logica API indipendente dal livello di presentazione +├── Database/ ← script di migrazione e seeders per i dati di test +├── Components/ ← componenti visivi condivisi nell'applicazione +├── Event/ ← utile se si utilizza un'architettura guidata dagli eventi +├── Mail/ ← modelli di e-mail e relativa logica +└── Utils/ ← classi di aiuto +\-- + +Per i componenti visivi condivisi usati nelle presentazioni di tutta l'applicazione, si può usare la cartella `app/Components` o `app/Controls`: + +/--pre +app/Components/ +├── Form/ ← componenti di moduli condivisi +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← componenti per gli elenchi di dati +│ └── DataGrid.php +└── Navigation/ ← elementi di navigazione + ├── Breadcrumbs.php + └── Menu.php +\-- + +Qui si trovano i componenti con una logica più complessa. Se si desidera condividere i componenti tra più progetti, è bene separarli in un pacchetto autonomo del compositore. + +Nella cartella `app/Mail` si può collocare la gestione delle comunicazioni via e-mail: + +/--pre +app/Mail/ +├── templates/ ← modelli di e-mail +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Mappatura dei presentatori .[#toc-presenter-mapping] +==================================================== + +La mappatura definisce le regole per derivare i nomi delle classi dai nomi dei presentatori. Vengono specificate nella [configurazione |configuration] sotto la chiave `application › mapping`. + +In questa pagina, abbiamo mostrato che collochiamo i presentatori nella cartella `app/Presentation` (o `app/UI`). Dobbiamo comunicare a Nette questa convenzione nel file di configurazione. Una riga è sufficiente: + +```neon +application: + mapping: App\Presentation\*\**Presenter +``` + +Come funziona la mappatura? Per capire meglio, immaginiamo prima un'applicazione senza moduli. Vogliamo che le classi del presentatore rientrino nello spazio dei nomi `App\Presentation`, in modo che il presentatore `Home` sia mappato sulla classe `App\Presentation\HomePresenter`. Questo si ottiene con questa configurazione: + +```neon +application: + mapping: App\Presentation\*Presenter +``` + +La mappatura funziona sostituendo l'asterisco nella maschera `App\Presentation\*Presenter` con il nome del presentatore `Home`, ottenendo il nome finale della classe `App\Presentation\HomePresenter`. Semplice! + +Tuttavia, come si vede negli esempi di questo e di altri capitoli, le classi del presentatore vengono collocate in sottodirectory eponime, ad esempio il presentatore `Home` viene mappato nella classe `App\Presentation\Home\HomePresenter`. Questo si ottiene raddoppiando i due punti (richiede Nette Application 3.2): + +```neon +application: + mapping: App\Presentation\**Presenter +``` + +Ora passiamo alla mappatura dei presentatori nei moduli. Possiamo definire una mappatura specifica per ogni modulo: + +```neon +application: + mapping: + Front: App\Presentation\Front\**Presenter + Admin: App\Presentation\Admin\**Presenter + Api: App\Api\*Presenter +``` + +In base a questa configurazione, il presentatore `Front:Home` si mappa alla classe `App\Presentation\Front\Home\HomePresenter`, mentre il presentatore `Api:OAuth` si mappa alla classe `App\Api\OAuthPresenter`. + +Poiché i moduli `Front` e `Admin` hanno un metodo di mappatura simile e probabilmente ci saranno altri moduli di questo tipo, è possibile creare una regola generale che li sostituisca. Un nuovo asterisco per il modulo sarà aggiunto alla maschera della classe: + +```neon +application: + mapping: + *: App\Presentation\*\**Presenter + Api: App\Api\*Presenter +``` + +Funziona anche per strutture di directory annidate più in profondità, come il presenter `Admin:User:Edit`, dove il segmento con l'asterisco si ripete per ogni livello e risulta nella classe `App\Presentation\Admin\User\Edit\EditPresenter`. + +Una notazione alternativa consiste nell'utilizzare un array composto da tre segmenti invece di una stringa. Questa notazione è equivalente alla precedente: + +```neon +application: + mapping: + *: [App\Presentation, *, **Presenter] + Api: [App\Api, '', *Presenter] +``` diff --git a/application/it/how-it-works.texy b/application/it/how-it-works.texy index bff0380317..cf4a5516d6 100644 --- a/application/it/how-it-works.texy +++ b/application/it/how-it-works.texy @@ -22,18 +22,18 @@ La struttura delle directory è simile a questa: /--pre web-project/ ├── app/ ← directory with application -│ ├── Presenters/ ← presenter classes -│ │ ├── HomePresenter.php ← Home presenter class -│ │ └── templates/ ← templates directory -│ │ ├── @layout.latte ← template of shared layout -│ │ └── Home/ ← templates for Home presenter -│ │ └── default.latte ← template for action `default` -│ ├── Router/ ← configuration of URL addresses +│ ├── Core/ ← classi di base necessarie +│ │ └── RouterFactory.php ← configurazione degli indirizzi URL +│ ├── Presentation/ ← presenter, template & co. +│ │ ├── @layout.latte ← modello di layout condiviso +│ │ └── Home/ ← cartella del presentatore Home +│ │ ├── HomePresenter.php ← Classe del presentatore della casa +│ │ └── default.latte ← template per l'azione default │ └── Bootstrap.php ← booting class Bootstrap ├── bin/ ← scripts for the command line ├── config/ ← configuration files │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← error logs ├── temp/ ← temporary files, cache, … ├── vendor/ ← libraries installed by Composer @@ -45,9 +45,9 @@ La struttura delle directory è simile a questa: └── .htaccess ← prohibits access to all directories except www \-- -È possibile modificare la struttura delle directory in qualsiasi modo, rinominare o spostare cartelle, e quindi modificare semplicemente i percorsi di `log/` e `temp/` nel file `Bootstrap.php` e il percorso di questo file in `composer.json` nella sezione `autoload`. Niente di più, nessuna riconfigurazione complicata, nessuna modifica costante. Nette ha un [rilevamento automatico intelligente |bootstrap#development-vs-production-mode]. +È possibile modificare la struttura delle directory a piacimento, rinominare o spostare le cartelle: è completamente flessibile. Nette è inoltre dotato di un rilevamento automatico intelligente e riconosce automaticamente la posizione dell'applicazione, compresa la sua base URL. -Per applicazioni un po' più grandi, possiamo dividere le cartelle con presentatori e modelli in sottodirectory (su disco) e in spazi di nomi (nel codice), che chiamiamo [moduli |modules]. +Per applicazioni un po' più grandi, possiamo organizzare le cartelle dei presentatori e dei modelli in [sottodirectory |directory-structure#Presenters and templates] e raggruppare le classi in spazi dei nomi, che chiamiamo moduli. La cartella `www/` è la cartella pubblica o document-root del progetto. È possibile rinominarla senza dover impostare nient'altro sul lato dell'applicazione. È sufficiente [configurare l'hosting |nette:troubleshooting#How to change or remove www directory from URL] in modo che la radice dei documenti vada a questa cartella. @@ -75,7 +75,7 @@ Il suo compito è: Che tipo di fabbrica? Non produciamo trattori, ma siti web! Aspettate, vi sarà spiegato subito. -Per "inizializzare l'ambiente" intendiamo, ad esempio, che venga attivato [Tracy |tracy:], uno strumento straordinario per la registrazione o la visualizzazione degli errori. Registra gli errori sul server di produzione e li visualizza direttamente sul server di sviluppo. Pertanto, l'inizializzazione deve anche decidere se il sito è in modalità di produzione o di sviluppo. A tale scopo, Nette utilizza il rilevamento automatico: se il sito viene eseguito su localhost, viene eseguito in modalità sviluppatore. Non è necessario configurare nulla e l'applicazione è pronta per lo sviluppo e la produzione. Questi passaggi sono eseguiti e descritti in dettaglio nel capitolo sulla [classe Bootstrap |bootstrap]. +Per "inizializzazione dell'ambiente" intendiamo, ad esempio, l'attivazione di [Tracy |tracy:], che è uno strumento fantastico per la registrazione e la visualizzazione degli errori. Sui server di produzione registra gli errori, mentre su quelli di sviluppo li visualizza direttamente. Pertanto, l'inizializzazione include la determinazione se il sito web viene eseguito in modalità di produzione o di sviluppo. A tale scopo, Nette utilizza [un rilevamento automatico intelligente |bootstrap#development-vs-production-mode]: se il sito viene eseguito su localhost, funziona in modalità di sviluppo. Non è necessaria alcuna configurazione e l'applicazione è pronta per lo sviluppo e la produzione. Questi passaggi vengono eseguiti e descritti in dettaglio nel capitolo sulle [classi Bootstrap |bootstrap]. Il terzo punto (sì, abbiamo saltato il secondo, ma ci torneremo) è l'avvio dell'applicazione. La gestione delle richieste HTTP in Nette è affidata alla classe `Nette\Application\Application` (di seguito denominata `Application`), quindi quando diciamo "avviare un'applicazione", intendiamo chiamare un metodo con il nome `run()` su un oggetto di questa classe. @@ -91,7 +91,7 @@ Le applicazioni scritte in Nette sono suddivise in molti cosiddetti presenter (i L'applicazione inizia chiedendo al cosiddetto router di decidere a quale dei presenter passare la richiesta corrente per l'elaborazione. Il router decide di chi è la responsabilità. Osserva l'URL di ingresso `https://example.com/product/123`, che vuole `show` un prodotto con `id: 123` come azione. È buona abitudine scrivere le coppie presentatore + azione separate da due punti come `Product:show`. -Quindi il router ha trasformato l'URL in una coppia `Presenter:action` + parametri, nel nostro caso `Product:show` + `id: 123`. Si può vedere l'aspetto di un router nel file `app/Router/RouterFactory.php` e lo descriveremo in dettaglio nel capitolo [Routing |Routing]. +Quindi il router ha trasformato l'URL in una coppia `Presenter:action` + parametri, nel nostro caso `Product:show` + `id: 123`. Si può vedere l'aspetto di un router nel file `app/Core/RouterFactory.php` e lo descriveremo in dettaglio nel capitolo [Routing |Routing]. Andiamo avanti. L'applicazione conosce già il nome del presentatore e può continuare. Creando un oggetto `ProductPresenter`, che è il codice del presentatore `Product`. Più precisamente, chiede al contenitore DI di creare il presentatore, perché produrre oggetti è il suo lavoro. @@ -121,12 +121,9 @@ Quindi, è stato chiamato il metodo `renderShow(123)`, il cui codice è un esemp Successivamente, il presentatore restituisce la risposta. Questa può essere una pagina HTML, un'immagine, un documento XML, l'invio di un file dal disco, JSON o il reindirizzamento a un'altra pagina. È importante notare che, se non si dice esplicitamente come rispondere (come nel caso di `ProductPresenter`), la risposta sarà il rendering del modello con una pagina HTML. Perché? Perché nel 99% dei casi vogliamo disegnare un modello, quindi il presentatore prende questo comportamento come predefinito e vuole semplificarci il lavoro. Questo è il punto di Nette. -Non dobbiamo nemmeno dichiarare quale modello disegnare, lui ricava il percorso per raggiungerlo secondo una semplice logica. Nel caso del presenter `Product` e dell'azione `show`, cerca di vedere se esiste uno di questi file di template relativi alla directory in cui si trova la classe `ProductPresenter`: +Non è nemmeno necessario specificare quale template rendere; il framework dedurrà da solo il percorso. Nel caso dell'azione `show`, cerca semplicemente di caricare il template `show.latte` nella cartella con la classe `ProductPresenter`. Cerca anche di trovare il layout nel file `@layout.latte` (maggiori informazioni sulla [ricerca dei template |templates#Template Lookup]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -Cercherà anche di trovare il layout nel file `@layout.latte` e quindi eseguirà il rendering del modello. Ora il compito del presentatore e dell'intera applicazione è completato. Se il modello non esiste, verrà restituita una pagina con errore 404. Per saperne di più sui presentatori, consultare la pagina [Presentatori |Presenters]. +Successivamente, i modelli vengono renderizzati. Questo completa il compito del presentatore e dell'intera applicazione e il lavoro è terminato. Se il modello non esistesse, verrebbe restituita una pagina di errore 404. Per saperne di più sui presentatori, consultare la pagina [Presentatori |presenters]. [* request-flow.svg *] @@ -137,7 +134,7 @@ Per sicurezza, proviamo a riepilogare l'intero processo con un URL leggermente d 3) il router decodifica l'URL come una coppia di oggetti `Home:default` 4) viene creato un oggetto `HomePresenter` 5) viene richiamato il metodo `renderDefault()` (se esiste) -6) viene reso un modello `templates/Home/default.latte` con un layout `templates/@layout.latte` +6) viene reso un modello `default.latte` con un layout `@layout.latte` Potreste esservi imbattuti in molti concetti nuovi, ma crediamo che abbiano un senso. Creare applicazioni in Nette è un gioco da ragazzi. diff --git a/application/it/modules.texy b/application/it/modules.texy deleted file mode 100644 index 55ee0c0696..0000000000 --- a/application/it/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Moduli -****** - -.[perex] -In Nette, i moduli rappresentano le unità logiche che compongono un'applicazione. Comprendono presentatori, modelli, eventualmente anche componenti e classi di modelli. - -Una cartella per i presentatori e una per i modelli non sarebbe sufficiente per i progetti reali. Avere decine di file in una cartella è quantomeno disorganizzato. Come uscirne? Semplicemente dividendoli in sottodirectory su disco e in spazi dei nomi nel codice. E questo è esattamente ciò che fanno i moduli Nette. - -Dimentichiamo quindi un'unica cartella per i presentatori e i modelli e creiamo invece dei moduli, ad esempio `Admin` e `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← directory with modules -│ ├── Admin/ ← module Admin -│ │ ├── Presenters/ ← its presenters -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← module Front -│ └── Presenters/ ← its presenters -│ └── ... -\-- - -Questa struttura di cartelle si rifletterà negli spazi dei nomi delle classi, quindi ad esempio `DashboardPresenter` sarà nello spazio dei nomi `App\Modules\Admin\Presenters`: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Il presentatore `Dashboard` all'interno del modulo `Admin` è referenziato all'interno dell'applicazione usando la notazione dei due punti come `Admin:Dashboard`, e la sua azione `default` come `Admin:Dashboard:default`. -E come fa Nette a sapere che `Admin:Dashboard` rappresenta la classe `App\Modules\Admin\Presenters\DashboardPresenter`? Questo è determinato dalla [mappatura |#mapping] nella configurazione. -Pertanto, la struttura data non è rigida e può essere modificata in base alle proprie esigenze. - -I moduli possono naturalmente contenere tutti gli altri elementi oltre ai presentatori e ai modelli, come componenti, classi di modelli, ecc. - - -Moduli annidati .[#toc-nested-modules] --------------------------------------- - -I moduli non devono formare solo una struttura piatta, ma si possono anche creare sottomoduli, ad esempio: - -/--pre -app/ -├── Modules/ ← directory with modules -│ ├── Blog/ ← module Blog -│ │ ├── Admin/ ← submodule Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← submodule Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← module Forum -│ │ └── ... -\-- - -Così, il modulo `Blog` è suddiviso nei sottomoduli `Admin` e `Front`. Anche in questo caso, ciò si rifletterà negli spazi dei nomi, che saranno `App\Modules\Blog\Admin\Presenters` e così via. Il presentatore `Dashboard` all'interno del sottomodulo viene chiamato `Blog:Admin:Dashboard`. - -L'annidamento può andare in profondità quanto si vuole, quindi si possono creare dei sottomoduli. - - -Creazione di collegamenti .[#toc-creating-links] ------------------------------------------------- - -I collegamenti nei modelli di presentatore sono relativi al modulo corrente. Pertanto, il collegamento `Foo:default` porta al presentatore `Foo` nello stesso modulo del presentatore corrente. Se il modulo corrente è `Front`, ad esempio, il collegamento si presenta in questo modo: - -```latte -link to Front:Product:show -``` - -Un collegamento è relativo anche se include il nome di un modulo, che viene considerato un sottomodulo: - -```latte -link to Front:Shop:Product:show -``` - -I collegamenti assoluti sono scritti in modo analogo ai percorsi assoluti su disco, ma con i due punti al posto degli slash. Pertanto, un collegamento assoluto inizia con i due punti: - -```latte -link to Admin:Product:show -``` - -Per scoprire se ci troviamo in un certo modulo o in un suo sottomodulo possiamo usare la funzione `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Instradamento .[#toc-routing] ------------------------------ - -Vedere il [capitolo sull'instradamento |routing#Modules]. - - -Mappatura .[#toc-mapping] -------------------------- - -Definisce le regole con cui il nome della classe viene derivato dal nome del presentatore. Vengono scritte nella [configurazione |configuration] sotto la chiave `application › mapping`. - -Cominciamo con un esempio che non usa moduli. Vogliamo solo che le classi del presentatore abbiano lo spazio dei nomi `App\Presenters`. Ciò significa che un presentatore come `Home` deve mappare alla classe `App\Presenters\HomePresenter`. Questo si può ottenere con la seguente configurazione: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -Il nome del presentatore viene sostituito con l'asterisco nella maschera della classe e il risultato è il nome della classe. Facile! - -Se dividiamo i presentatori in moduli, possiamo avere la nostra mappatura per ogni modulo: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Ora il presentatore `Front:Home` mappa alla classe `App\Modules\Front\Presenters\HomePresenter` e il presentatore `Admin:Dashboard` alla classe `App\Modules\Admin\Presenters\DashboardPresenter`. - -È più pratico creare una regola generale (asterisco) per sostituire le prime due. L'asterisco in più sarà aggiunto alla maschera di classe solo per il modulo: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Ma cosa succede se utilizziamo moduli annidati e abbiamo un presentatore `Admin:User:Edit`? In questo caso, il segmento con l'asterisco che rappresenta il modulo per ogni livello viene semplicemente ripetuto e il risultato è la classe `App\Modules\Admin\User\Presenters\EditPresenter`. - -Una notazione alternativa consiste nell'utilizzare un array composto da tre segmenti invece di una stringa. Questa notazione è equivalente alla precedente: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -Il valore predefinito è `*: *Module\*Presenter`. diff --git a/application/it/presenters.texy b/application/it/presenters.texy index 687f5d900c..7f12f07c7f 100644 --- a/application/it/presenters.texy +++ b/application/it/presenters.texy @@ -60,7 +60,7 @@ Simile al metodo `render()`. Mentre `render()` è destinato a prepar È importante che `action()` sia chiamato prima di `render()`quindi al suo interno si può eventualmente modificare il corso successivo del ciclo di vita, cioè cambiare il template che sarà reso e anche il metodo `render()` che sarà chiamato, utilizzando `setView('otherView')`. -I parametri della richiesta vengono passati al metodo. È possibile e consigliabile specificare i tipi di parametri, ad esempio `actionShow(int $id, string $slug = null)` - se il parametro `id` manca o non è un intero, il presentatore restituisce l'[errore 404 |#Error 404 etc.] e termina l'operazione. +I parametri della richiesta vengono passati al metodo. È possibile e consigliabile specificare i tipi di parametri, ad esempio `actionShow(int $id, ?string $slug = null)` - se il parametro `id` manca o non è un intero, il presentatore restituisce l'[errore 404 |#Error 404 etc.] e termina l'operazione. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ Nel modello, questi messaggi sono disponibili nella variabile `$flashes` come og Errore 404 ecc. .[#toc-error-404-etc] ===================================== -Quando non possiamo soddisfare la richiesta, perché ad esempio l'articolo che vogliamo visualizzare non esiste nel database, lanceremo l'errore 404 usando il metodo `error(string $message = null, int $httpCode = 404)`, che rappresenta l'errore HTTP 404: +Quando non possiamo soddisfare la richiesta, perché ad esempio l'articolo che vogliamo visualizzare non esiste nel database, lanceremo l'errore 404 usando il metodo `error(?string $message = null, int $httpCode = 404)`, che rappresenta l'errore HTTP 404: ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Parametri di richiesta .[#toc-request-parameters] +================================================= + +Il presentatore, così come ogni componente, ottiene i suoi parametri dalla richiesta HTTP. I loro valori possono essere recuperati con il metodo `getParameter($name)` o `getParameters()`. I valori sono stringhe o array di stringhe, essenzialmente dati grezzi ottenuti direttamente dall'URL. + +Per maggiore comodità, si consiglia di rendere i parametri accessibili tramite proprietà. È sufficiente annotarli con l'attributo `#[Parameter]` attributo: + +```php +use Nette\Application\Attributes\Parameter; // questa linea è importante + +class HomePresenter extends Nette\Application\UI\Presenter +{ + #[Parameter] + public string $theme; // deve essere pubblica +} +``` + +Per le proprietà, si consiglia di specificare il tipo di dati (ad esempio, `string`). Nette calcolerà automaticamente il valore in base ad esso. Anche i valori dei parametri possono essere [convalidati |#Validation of Parameters]. + +Quando si crea un collegamento, è possibile impostare direttamente il valore dei parametri: + +```latte +click +``` + + Parametri persistenti .[#toc-persistent-parameters] =================================================== @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Se `$this->lang` ha un valore come `'en'`, i link creati con `link()` o `n:href` conterranno anche il parametro `lang=en`. E quando il link viene cliccato, sarà di nuovo `$this->lang = 'en'`. -Per le proprietà, si consiglia di includere il tipo di dati (ad esempio, `string`) e si può anche includere un valore predefinito. I valori dei parametri possono essere [convalidati |#Validation of Persistent Parameters]. +Per le proprietà, si consiglia di includere il tipo di dati (ad esempio, `string`) e si può anche includere un valore predefinito. I valori dei parametri possono essere [convalidati |#Validation of Parameters]. I parametri persistenti vengono passati per impostazione predefinita tra tutte le azioni di un determinato presentatore. Per passarli tra più presentatori, è necessario definirli: @@ -307,18 +333,12 @@ Approfondimento .[#toc-going-deeper] Quanto mostrato finora in questo capitolo sarà probabilmente sufficiente. Le righe che seguono sono destinate a chi è interessato ad approfondire i presentatori e vuole sapere tutto. -Requisiti e parametri .[#toc-requirement-and-parameters] +Convalida dei parametri .[#toc-validation-of-parameters] -------------------------------------------------------- -La richiesta gestita dal presentatore è l'oggetto [api:Nette\Application\Request] e viene restituita dal metodo del presentatore `getRequest()`. Include un array di parametri e ognuno di essi appartiene o a qualche componente o direttamente al presentatore (che in realtà è anch'esso un componente, anche se speciale). Quindi Nette ridistribuisce i parametri e passa tra i singoli componenti (e il presentatore) chiamando il metodo `loadState(array $params)`. I parametri possono essere ottenuti con il metodo `getParameters(): array`, singolarmente con `getParameter($name)`. I valori dei parametri sono stringhe o array di stringhe, in pratica dati grezzi ottenuti direttamente da un URL. +I valori dei [parametri della richiesta |#request parameters] e dei [parametri persistenti |#persistent parameters] ricevuti dagli URL vengono scritti nelle proprietà dal metodo `loadState()`. Il metodo controlla anche se il tipo di dati specificato nella proprietà corrisponde, altrimenti risponde con un errore 404 e la pagina non viene visualizzata. - -Convalida dei parametri persistenti .[#toc-validation-of-persistent-parameters] -------------------------------------------------------------------------------- - -I valori dei [parametri persistenti |#persistent parameters] ricevuti dagli URL vengono scritti nelle proprietà dal metodo `loadState()`. Il metodo controlla anche se il tipo di dati specificato nella proprietà corrisponde, altrimenti risponde con un errore 404 e la pagina non viene visualizzata. - -Non fidarsi mai ciecamente dei parametri persistenti, perché possono essere facilmente sovrascritti dall'utente nell'URL. Ad esempio, è così che si controlla se `$this->lang` è tra le lingue supportate. Un buon modo per farlo è sovrascrivere il metodo `loadState()` citato in precedenza: +Non fidarsi mai ciecamente dei parametri, perché possono essere facilmente sovrascritti dall'utente nell'URL. Ad esempio, è così che controlliamo se `$this->lang` è tra le lingue supportate. Un buon modo per farlo è sovrascrivere il metodo `loadState()` citato in precedenza: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,7 +361,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Salvare e ripristinare la richiesta .[#toc-save-and-restore-the-request] ------------------------------------------------------------------------ -È possibile salvare la richiesta corrente in una sessione o ripristinarla dalla sessione e lasciare che il presentatore la esegua di nuovo. Ciò è utile, ad esempio, quando un utente compila un modulo e il suo login scade. Per non perdere i dati, prima di reindirizzare alla pagina di accesso, salviamo la richiesta corrente nella sessione usando `$reqId = $this->storeRequest()`, che restituisce un identificatore sotto forma di stringa breve e lo passa come parametro al presentatore dell'accesso. +La richiesta che il presentatore gestisce è un oggetto [api:Nette\Application\Request] e viene restituita dal metodo del presentatore `getRequest()`. + +È possibile salvare la richiesta corrente in una sessione o ripristinarla dalla sessione e lasciare che il presentatore la esegua di nuovo. Ciò è utile, ad esempio, quando un utente compila un modulo e il suo login scade. Per non perdere i dati, prima di reindirizzare alla pagina di accesso, salviamo la richiesta corrente nella sessione con il metodo `$reqId = $this->storeRequest()`, che restituisce un identificatore sotto forma di stringa breve e lo passa come parametro al presentatore di accesso. Dopo l'accesso, chiamiamo il metodo `$this->restoreRequest($reqId)`, che preleva la richiesta dalla sessione e la inoltra ad essa. Il metodo verifica che la richiesta sia stata creata dallo stesso utente che ha effettuato l'accesso. Se un altro utente accede o la chiave non è valida, non fa nulla e il programma continua. @@ -362,7 +384,7 @@ Il reindirizzamento non avviene con una richiesta AJAX o POST, perché comporter Si può anche invocare la canonizzazione manualmente con il metodo `canonicalize()`, che, come il metodo `link()`, riceve come argomenti il presentatore, le azioni e i parametri. Crea un link e lo confronta con l'URL corrente. Se è diverso, reindirizza al link generato. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // reindirizza se $slug è diverso da $realSlug @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Limitazione dell'accesso tramite `#[Requires]` .[#toc-access-restriction-using-requires]{data-version:3.2.2} +------------------------------------------------------------------------------------------------------------ + +L'attributo `#[Requires]` fornisce opzioni avanzate per limitare l'accesso ai presentatori e ai loro metodi. Può essere usato per specificare metodi HTTP, richiedere richieste AJAX, limitare l'accesso alla stessa origine e limitare l'accesso al solo inoltro. L'attributo può essere applicato alle classi di presentatori e ai singoli metodi, come ad esempio `action()`, `render()`, `handle()`, e `createComponent()`. + +È possibile specificare queste restrizioni: +- sui metodi HTTP: `#[Requires(methods: ['GET', 'POST'])]` +- che richiedono una richiesta AJAX: `#[Requires(ajax: true)]` +- accesso solo dalla stessa origine: `#[Requires(sameOrigin: true)]` +- accesso solo tramite inoltro: `#[Requires(forward: true)]` +- restrizioni su azioni specifiche: `#[Requires(actions: 'default')]` + +Per i dettagli, vedere [Come usare l'attributo Requires |best-practices:attribute-requires]. + + +Controllo del metodo HTTP .[#toc-http-method-check] +--------------------------------------------------- + +In Nette, i presentatori verificano automaticamente il metodo HTTP di ogni richiesta in arrivo, principalmente per motivi di sicurezza. Per impostazione predefinita, sono ammessi i metodi `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. + +Se si desidera abilitare altri metodi, come ad esempio `OPTIONS`, è possibile utilizzare l'attributo `#[Requires]` (dall'applicazione Nette v3.2): + +```php +#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] +class MyPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Nella versione 3.1, la verifica viene eseguita in `checkHttpMethod()`, che controlla se il metodo specificato nella richiesta è incluso nell'array `$presenter->allowedMethods`. Aggiungere un metodo come questo: + +```php +class MyPresenter extends Nette\Application\UI\Presenter +{ + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } +} +``` + +È fondamentale sottolineare che se si abilita il metodo `OPTIONS`, è necessario gestirlo correttamente anche nel presenter. Questo metodo è spesso usato come una cosiddetta richiesta di preflight, che i browser inviano automaticamente prima della richiesta vera e propria quando è necessario determinare se la richiesta è consentita dal punto di vista della politica CORS (Cross-Origin Resource Sharing). Se si consente questo metodo ma non si implementa una risposta appropriata, si possono creare incongruenze e potenziali problemi di sicurezza. + + Ulteriori letture .[#toc-further-reading] ========================================= diff --git a/application/it/routing.texy b/application/it/routing.texy index c01d4ac823..8581ad5a62 100644 --- a/application/it/routing.texy +++ b/application/it/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Oppure possiamo usare questa forma, notando la riscrittura dell'espressione regolare di validazione: +Per una specifica più dettagliata, si può usare una forma ancora più estesa, in cui oltre ai valori predefiniti si possono impostare altre proprietà dei parametri, come un'espressione regolare di validazione (vedere il parametro `id` ): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Questi formati più loquaci sono utili per aggiungere altri metadati. +È importante notare che se i parametri definiti nell'array non sono inclusi nella maschera del percorso, i loro valori non possono essere modificati, nemmeno utilizzando i parametri di query specificati dopo un punto interrogativo nell'URL. Filtri e traduzioni .[#toc-filters-and-translations] @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Moduli .[#toc-modules] ---------------------- -Se abbiamo più rotte che appartengono a un [modulo |modules], possiamo usare `withModule()` per raggrupparle: +Se abbiamo più rotte che appartengono a un [modulo |directory-structure#Presenters and Templates], possiamo usare `withModule()` per raggrupparle: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Integrazione .[#toc-integration] ================================ -Per collegare il nostro router all'applicazione, dobbiamo comunicarlo al contenitore DI. Il modo più semplice è preparare il factory che costruirà l'oggetto router e dire al contenitore di configurazione di usarlo. Diciamo quindi di scrivere un metodo a questo scopo `App\Router\RouterFactory::createRouter()`: +Per collegare il nostro router all'applicazione, dobbiamo comunicarlo al contenitore DI. Il modo più semplice è preparare il factory che costruirà l'oggetto router e dire al contenitore di configurazione di usarlo. Diciamo quindi di scrivere un metodo a questo scopo `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ Poi scriviamo nella [configurazione |dependency-injection:services]: ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Tutte le dipendenze, come la connessione al database, ecc. vengono passate al metodo factory come parametri, utilizzando l'[autowiring |dependency-injection:autowiring]: @@ -663,7 +663,7 @@ Per uso separato si intende l'uso delle funzionalità del router in un'applicazi Quindi creeremo di nuovo un metodo che costruirà un router, ad esempio: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Oppure creeremo direttamente gli oggetti: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/it/templates.texy b/application/it/templates.texy index bb704a88f2..5bbf287b39 100644 --- a/application/it/templates.texy +++ b/application/it/templates.texy @@ -34,35 +34,81 @@ E questo potrebbe essere il modello di azione: Definisce il blocco `content`, che viene inserito al posto di `{include content}` nel layout, e ridefinisce anche il blocco `title`, che sovrascrive `{block title}` nel layout. Provate a immaginare il risultato. -Ricerca dei modelli .[#toc-search-for-templates] ------------------------------------------------- +Ricerca di modelli .[#toc-template-lookup] +------------------------------------------ + +Nei presentatori, non è necessario specificare quale template debba essere reso; il framework determinerà automaticamente il percorso, semplificando la codifica. + +Se si utilizza una struttura di cartelle in cui ogni presentatore ha una propria cartella, è sufficiente posizionare il template in questa cartella sotto il nome dell'azione (cioè della vista). Ad esempio, per l'azione `default`, utilizzare il modello `default.latte`: -Il percorso dei modelli viene dedotto secondo una semplice logica. Si cerca di vedere se uno di questi file di template esiste relativamente alla directory in cui si trova la classe del presentatore, dove `` è il nome del presentatore corrente e `` è il nome dell'azione corrente: +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -- `templates//.latte` -- `templates/..latte` +Se si utilizza una struttura in cui i presentatori sono riuniti in una directory e i modelli in una cartella `templates`, salvare il tutto in un file `..latte` oppure `/.latte`: -Se il modello non viene trovato, si cercherà nella cartella `templates` a un livello superiore, cioè allo stesso livello della cartella con la classe del presentatore. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- -Se il modello non viene trovato nemmeno lì, la risposta è un [errore 404 |presenters#Error 404 etc.]. +La directory `templates` può anche essere collocata un livello più in alto, allo stesso livello della directory con le classi dei presentatori. -Si può anche cambiare la vista usando `$this->setView('otherView')`. Oppure, invece di cercare, specificare direttamente il nome del file del template usando `$this->template->setFile('/path/to/template.latte')`. +Se il modello non viene trovato, il presentatore risponde con un [errore 404 - pagina non trovata |presenters#Error 404 etc]. + +È possibile modificare la vista utilizzando `$this->setView('anotherView')`. È anche possibile specificare direttamente il file del modello con `$this->template->setFile('/path/to/template.latte')`. .[note] -È possibile modificare i percorsi in cui vengono cercati i modelli sovrascrivendo il metodo [formatTemplateFiles |api:Nette\Application\UI\Presenter::formatTemplateFiles()], che restituisce un array di possibili percorsi di file. +I file in cui vengono cercati i modelli possono essere modificati sovrascrivendo il metodo [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], che restituisce un array di possibili nomi di file. + + +Ricerca dei modelli di layout .[#toc-layout-template-lookup] +------------------------------------------------------------ + +Nette cerca automaticamente anche il file di layout. + +Se si utilizza una struttura di directory in cui ogni presentatore ha una propria directory, collocare il layout nella cartella del presentatore, se è specifico solo per lui, oppure a un livello superiore se è comune a più presentatori: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +Se si utilizza una struttura in cui i presentatori sono raggruppati in una directory e i modelli si trovano in una cartella `templates`, il layout sarà previsto nei seguenti punti: -Il layout è previsto nei seguenti file: +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` layout comune a più presentatori +Se il presentatore si trova in un modulo, cercherà anche più in alto nell'albero delle directory, in base alla nidificazione del modulo. -`` è il nome del presentatore corrente e `` è il nome del layout, che per impostazione predefinita è `'layout'`. Il nome può essere modificato con `$this->setLayout('otherLayout')`, in modo da provare i file `@otherLayout.latte`. +Il nome del layout può essere modificato con `$this->setLayout('layoutAdmin')` e sarà previsto nel file `@layoutAdmin.latte`. È anche possibile specificare direttamente il file del modello di layout usando `$this->setLayout('/path/to/template.latte')`. -È anche possibile specificare direttamente il nome del file del modello di layout con `$this->setLayout('/path/to/template.latte')`. L'uso di `$this->setLayout(false)` disabilita la ricerca dei layout. +L'uso di `$this->setLayout(false)` o del tag `{layout none}` all'interno del template disabilita la ricerca del layout. .[note] -È possibile modificare i percorsi in cui vengono cercati i modelli sovrascrivendo il metodo [formatLayoutTemplateFiles |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], che restituisce un array di possibili percorsi di file. +I file in cui vengono cercati i modelli di layout possono essere modificati sovrascrivendo il metodo [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], che restituisce un array di possibili nomi di file. Variabili nel modello .[#toc-variables-in-the-template] @@ -104,7 +150,7 @@ L'annotazione `@property-read` è per l'IDE e l'analisi statica, farà funzionar Ci si può concedere il lusso di sussurrare anche nei template, basta installare il plugin Latte in PhpStorm e specificare il nome della classe all'inizio del template, si veda l'articolo "Latte: come digitare il sistema":https://blog.nette.org/it/latte-come-usare-il-sistema-di-tipi: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void La versione 3 di Latte offre un metodo più avanzato, creando un'[estensione |latte:creating-extension] per ogni progetto web. Ecco un esempio approssimativo di una classe di questo tipo: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ La registriamo usando [configuration |configuration#Latte]: ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ In alternativa, il traduttore può essere impostato utilizzando la [configurazio ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` Il traduttore può essere utilizzato, ad esempio, come filtro `|translate`, con parametri aggiuntivi passati al metodo `translate()` (vedere `foo, bar`): diff --git a/application/pl/@home.texy b/application/pl/@home.texy index 138c335e70..76169bf852 100644 --- a/application/pl/@home.texy +++ b/application/pl/@home.texy @@ -1,24 +1,12 @@ -Nette Application -***************** +Aplikacja Nette +*************** .[perex] -Pakiet `nette/application` stanowi podstawę do tworzenia interaktywnych aplikacji internetowych. +Nette Application to rdzeń frameworka Nette, który zapewnia potężne narzędzia do tworzenia nowoczesnych aplikacji internetowych. Oferuje wiele wyjątkowych funkcji, które znacznie upraszczają rozwój oraz poprawiają bezpieczeństwo i łatwość konserwacji kodu. -- [Jak działają aplikacje? |how-it-works] -- [Bootstrap |Bootstrap] -- [Prezenterzy |presenters] -- [Szablony |templates] -- [Moduły |modules] -- [Trasowanie |routing] -- [Tworzenie linków URL |creating-links] -- [Elementy interaktywne |components] -- [AJAX i snippety |ajax] -- [Mnożnik |multiplier] -- [Konfiguracja |configuration] - -Instalacja ----------- +Instalacja .[#toc-installation] +------------------------------- Pobierz i zainstaluj bibliotekę za pomocą [Composera |best-practices:composer]: @@ -26,11 +14,72 @@ Pobierz i zainstaluj bibliotekę za pomocą [Composera |best-practices:composer] composer require nette/application ``` -| wersja zgodna z PHP + +Dlaczego warto wybrać aplikację Nette? .[#toc-why-choose-nette-application] +--------------------------------------------------------------------------- + +Nette zawsze było pionierem w technologiach internetowych. + +**Bidirectional Router:** Nette posiada zaawansowany system routingu unikalny w swojej dwukierunkowości - nie tylko tłumaczy adresy URL na działania aplikacji, ale może również generować adresy URL w odwrotnej kolejności. Oznacza to, że: +- Możesz modyfikować strukturę adresów URL całej aplikacji w dowolnym momencie bez modyfikowania plików szablonów +- Adresy URL są automatycznie kanonizowane, co poprawia SEO +- Routing jest zdefiniowany w jednym miejscu, a nie rozproszony w adnotacjach + +**Komponenty i sygnały:** Wbudowany system komponentów inspirowany Delphi i React.js jest unikalny wśród frameworków PHP: +- Umożliwia tworzenie elementów UI wielokrotnego użytku +- Obsługuje hierarchiczną kompozycję komponentów +- Oferuje elegancką obsługę żądań AJAX za pomocą sygnałów +- Bogata biblioteka gotowych komponentów na [Componette](https://componette.org). + +**AJAX i Snippets:** Nette wprowadziło rewolucyjny sposób pracy z AJAX w 2009 roku, przed rozwiązaniami takimi jak Hotwire dla Ruby on Rails czy Symfony UX Turbo: +- Snippets umożliwiają aktualizację tylko części strony bez pisania JavaScript +- Automatyczna integracja z systemem komponentów +- Inteligentne unieważnianie sekcji strony +- Minimalny transfer danych + +**Intuicyjne szablony [Latte |latte:]:** Najbezpieczniejszy system szablonów dla PHP z zaawansowanymi funkcjami: +- Automatyczna ochrona XSS z ucieczką kontekstową +- Możliwość rozbudowy o niestandardowe filtry, funkcje i tagi +- Dziedziczenie szablonów i fragmenty dla AJAX +- Doskonała obsługa PHP 8.x z systemem typów + +**Wstrzykiwanie zależności:** Nette w pełni wykorzystuje wstrzykiwanie zależności: +- Automatyczne przekazywanie zależności (autowiring) +- Konfiguracja przy użyciu przejrzystego formatu NEON +- Wsparcie dla fabryk komponentów + + +Główne korzyści .[#toc-main-benefits] +------------------------------------- + +- Bezpieczeństwo**: Automatyczna ochrona przed [lukami |nette:vulnerability-protection] w zabezpieczeniach, takimi jak XSS, CSRF itp. +- **Produktywność**: Mniej pisania, więcej funkcji dzięki inteligentnemu projektowi +- **Debugowanie**: [Tracy debugger |tracy:] z panelem routingu +- Wydajność**: Inteligentny system buforowania, leniwe ładowanie komponentów +- Elastyczność**: Łatwa modyfikacja adresów URL nawet po ukończeniu aplikacji +- Komponenty**: Unikalny system elementów UI wielokrotnego użytku +- Nowoczesność**: Pełne wsparcie dla PHP 8.4+ i systemu typów + + +Pierwsze kroki .[#toc-getting-started] +-------------------------------------- + +1. [Zrozumienie aplikacji |how-it-works] - zrozumienie podstawowej architektury +2. [Prezenterzy |presenters] - Praca z prezenterami i akcjami +3. [Szablony |templates] - Tworzenie szablonów w Latte +4. [Routing |routing] - konfiguracja adresów URL +5. [Komponenty interaktywne |components] - korzystanie z systemu komponentów + + +Kompatybilność z PHP .[#toc-php-compatibility] +---------------------------------------------- + +| Wersja | kompatybilna z PHP |-----------|------------------- -| Nette Application 4.0 | PHP 8.0 - 8.2 -| Nette Application 3.1 | PHP 7.2 - 8.2 +| Nette Application 4.0 | PHP 8.1 - 8.4 +| Nette Application 3.2 | PHP 8.1 - 8.4 +| Nette Application 3.1 | PHP 7.2 - 8.3 | Nette Application 3.0 | PHP 7.1 - 8.0 | Nette Application 2.4 | PHP 5.6 - 8.0 -Ważne dla najnowszej wersji poprawki. +Obowiązuje dla najnowszych wersji poprawek. \ No newline at end of file diff --git a/application/pl/@left-menu.texy b/application/pl/@left-menu.texy index b150490a16..aae6dd2f11 100644 --- a/application/pl/@left-menu.texy +++ b/application/pl/@left-menu.texy @@ -4,7 +4,7 @@ Aplikacje w Nette - [Bootstrap |Bootstrap] - [Prezenterzy |presenters] - [Szablony |templates] -- [Moduły |modules] +- [Struktura katalogów |directory-structure] - [Trasowanie |routing] - [Tworzenie linków URL |creating-links] - [Elementy interaktywne |components] diff --git a/application/pl/ajax.texy b/application/pl/ajax.texy index 4b42eac748..9a6f09bd03 100644 --- a/application/pl/ajax.texy +++ b/application/pl/ajax.texy @@ -1,12 +1,12 @@ -AJAX i snippety -*************** +AJAX i fragmenty +****************
    -Nowoczesne aplikacje internetowe działają dziś w połowie na serwerze, w połowie w przeglądarce. AJAX jest kluczowym elementem łączącym. Jakie wsparcie oferuje Nette Framework? -- Wysyłanie fragmentów szablonów -- przekazywanie zmiennych między PHP a JavaScriptem -- debugowanie aplikacji AJAX +W dobie nowoczesnych aplikacji internetowych, w których funkcjonalność często rozciąga się między serwerem a przeglądarką, AJAX jest niezbędnym elementem łączącym. Jakie możliwości w tym zakresie oferuje Nette Framework? +- wysyłanie części szablonu, tzw. snippetów +- przekazywanie zmiennych między PHP i JavaScript +- narzędzia do debugowania żądań AJAX
    @@ -14,29 +14,32 @@ Nowoczesne aplikacje internetowe działają dziś w połowie na serwerze, w poł Żądanie AJAX .[#toc-ajax-request] ================================= -Żądanie AJAX nie różni się od klasycznego żądania - prezenter jest wywoływany z określonym widokiem i parametrami. Również od prezentera zależy, jak na nie odpowie: może użyć własnej procedury, która zwraca fragment kodu HTML (HTML snippet), dokument XML, obiekt JSON lub kod JavaScript. +Żądanie AJAX zasadniczo nie różni się od klasycznego żądania HTTP. Prezenter jest wywoływany z określonymi parametrami. To od prezentera zależy, jak odpowie na żądanie - może zwrócić dane w formacie JSON, wysłać fragment kodu HTML, dokument XML itp. -Po stronie serwera żądanie AJAX może zostać wykryte za pomocą metody serwisowej [obudowującej żądanie HTTP |http:request] `$httpRequest->isAjax()` (wykrywa na podstawie nagłówka HTTP `X-Requested-With`). Wewnątrz prezentera dostępny jest skrót w postaci metody `$this->isAjax()`. +Po stronie przeglądarki inicjujemy żądanie AJAX za pomocą funkcji `fetch()`: -Aby wysłać dane do przeglądarki w formacie JSON, możesz użyć gotowego obiektu `payload`: - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // przetwarzanie odpowiedzi +}); ``` -Jeśli potrzebujesz pełnej kontroli nad wysłanym JSON, użyj metody `sendJson` w prezenterze. Spowoduje to natychmiastowe zakończenie prezentera i zrezygnowanie z szablonu: +Po stronie serwera żądanie AJAX jest rozpoznawane przez metodę `$httpRequest->isAjax()` usługi [hermetyzującej żądanie HTTP |http:request]. Używa ona nagłówka HTTP `X-Requested-With`, więc jest niezbędna do jego wysłania. W prezenterze można użyć metody `$this->isAjax()`. + +Jeśli chcesz wysłać dane w formacie JSON, użyj metody [`sendJson()` |presenters#Sending a response] . Metoda ta kończy również aktywność prezentera. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Jeśli chcemy wysłać HTML, możemy wybrać specjalny szablon dla AJAX: +Jeśli planujesz odpowiedzieć za pomocą specjalnego szablonu zaprojektowanego dla AJAX, możesz to zrobić w następujący sposób: ```php public function handleClick($param): void @@ -44,27 +47,43 @@ public function handleClick($param): void if ($this->isAjax()) { $this->template->setFile('path/to/ajax.latte'); } - // ... + //... } ``` +Fragmenty .[#toc-snippets] +========================== + +Najpotężniejszym narzędziem oferowanym przez Nette do łączenia serwera z klientem są snippety. Dzięki nim można przekształcić zwykłą aplikację w aplikację AJAX przy minimalnym wysiłku i kilku linijkach kodu. Przykład Fifteen pokazuje, jak to wszystko działa, a jego kod można znaleźć na [GitHubie |https://github.com/nette-examples/fifteen]. + +Snippety lub wycinki pozwalają aktualizować tylko części strony, zamiast przeładowywać całą stronę. Jest to szybsze i bardziej wydajne, a także zapewnia wygodniejsze wrażenia użytkownika. Snippety mogą przypominać Hotwire dla Ruby on Rails lub Symfony UX Turbo. Co ciekawe, Nette wprowadziło snippety 14 lat wcześniej. + +Jak działają snippety? Kiedy strona jest ładowana po raz pierwszy (żądanie inne niż AJAX), ładowana jest cała strona, w tym wszystkie snippety. Gdy użytkownik wchodzi w interakcję ze stroną (np. klika przycisk, przesyła formularz itp.), zamiast ładowania całej strony, wykonywane jest żądanie AJAX. Kod w prezenterze wykonuje akcję i decyduje, które fragmenty wymagają aktualizacji. Nette renderuje te fragmenty i wysyła je w postaci tablicy JSON. Kod obsługi w przeglądarce wstawia następnie otrzymane fragmenty z powrotem na stronę. W związku z tym przesyłany jest tylko kod zmienionych fragmentów, co oszczędza przepustowość i przyspiesza ładowanie w porównaniu do przesyłania całej zawartości strony. + + Naja .[#toc-naja] -================= +----------------- -[Biblioteka Naja |https://naja.js.org] służy do obsługi żądań AJAX po stronie przeglądarki. [Zainstaluj |https://naja.js.org/#/guide/01-install-setup-naja] go jako pakiet node.js (do użytku z Webpack, Rollup, Vite, Parcel i innych): +Do obsługi snippetów po stronie przeglądarki używana jest [biblioteka Na |https://naja.js.org] ja. Należy [ją zainstal |https://naja.js.org/#/guide/01-install-setup-naja] ować jako pakiet node.js (do użytku z aplikacjami takimi jak Webpack, Rollup, Vite, Parcel i innymi): ```shell npm install naja ``` -...lub osadzić bezpośrednio w szablonie strony: +... lub wstawić ją bezpośrednio do szablonu strony: ```html ``` -Aby utworzyć żądanie AJAX ze zwykłego linku (sygnału) lub submitu formularza, wystarczy oznaczyć odpowiedni link, formularz lub przycisk klasą `ajax`: +Najpierw należy [zainicjować |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] bibliotekę: + +```js +naja.initialize(); +``` + +Aby uczynić zwykły link (sygnał) lub przesłanie formularza żądaniem AJAX, wystarczy oznaczyć odpowiedni link, formularz lub przycisk klasą `ajax`: ```html Go @@ -74,64 +93,39 @@ Aby utworzyć żądanie AJAX ze zwykłego linku (sygnału) lub submitu formularz or +
    ``` -Snippets -======== - -Znacznie potężniejszym narzędziem jest wbudowana obsługa snippetów AJAX. Dzięki niemu można zamienić zwykłą aplikację w AJAXową za pomocą zaledwie kilku linijek kodu. Przykład Fifteen, którego kod można znaleźć na [GitHubie |https://github.com/nette-examples/fifteen], demonstruje jak to działa. - -Działanie snippetów polega na tym, że na początkowym (czyli nie-AJAX-owym) żądaniu przenoszona jest cała strona, a następnie na każdym [podżądaniu |components#Signal] AJAX-owym (= żądanie do tego samego prezentera i widoku) przenoszony jest tylko kod zmienionych fragmentów we wspomnianym repozytorium `payload`. Istnieją dwa mechanizmy tego działania: unieważnianie i renderowanie snippetów. - -Snippets mogą przypominać Hotwire dla Ruby on Rails lub Symfony UX Turbo, ale Nette wymyślił je czternaście lat wcześniej. - +Przerysowywanie fragmentów .[#toc-redrawing-snippets] +----------------------------------------------------- -Unieważnianie fragmentów .[#toc-invalidation-of-snippets] -========================================================= - -Każdy obiekt klasy [Control |components] (którą jest sam Presenter) potrafi zapamiętać, czy w sygnale zaszły zmiany wymagające jego przerysowania. Służy do tego para metod `redrawControl()` i `isControlInvalid()`. Przykład: +Każdy obiekt klasy [Control |components] (w tym sam Presenter) przechowuje informacje o tym, czy wystąpiły zmiany, które wymagają jego przerysowania. W tym celu wykorzystywana jest metoda `redrawControl()`. ```php public function handleLogin(string $user): void { - // po zalogowaniu się użytkownika obiekt musi zostać przerysowany + // po zalogowaniu konieczne jest przerysowanie odpowiedniego fragmentu $this->redrawControl(); - // ... + //... } ``` -Nette oferuje jednak jeszcze dokładniejszą rozdzielczość niż na poziomie komponentów. Metody te mogą przyjąć jako argument nazwę "snippet", czyli wycinka. Możliwe jest więc unieważnienie (czyli wymuszenie przerysowania) na poziomie tych wycinków (każdy obiekt może mieć dowolną liczbę wycinków). Jeśli cały komponent zostanie unieważniony, każdy wycinek zostanie przerysowany. Komponent jest "unieważniony" nawet jeśli podkomponent jest unieważniony. - -```php -$this->isControlInvalid(); // -> false -$this->redrawControl('header'); // unieważnia snippet 'header' -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> prawda, przynajmniej jeden fragment jest nieprawidłowy +Nette pozwala również na dokładniejszą kontrolę tego, co wymaga przerysowania. Wspomniana metoda może przyjąć nazwę fragmentu jako argument. W ten sposób możliwe jest unieważnienie (czyli wymuszenie przerysowania) na poziomie części szablonu. Jeśli cały komponent zostanie unieważniony, każdy jego fragment zostanie również przerysowany: -$this->redrawControl(); // unieważnia cały komponent, każdy fragment -$this->isControlInvalid('footer'); // -> true +```php +// unieważnia fragment "nagłówka +$this->redrawControl('header'); ``` -Komponent, który otrzymuje sygnał, jest automatycznie oznaczany jako wyłączony. - -Unieważniając snippety, wiemy dokładnie, które części których elementów będą musiały zostać przerysowane. - - -Tagi `{snippet} … {/snippet}` .{toc: Tag snippet} -================================================= - -Renderowanie strony jest bardzo podobne do normalnego żądania: ładowane są te same szablony itp. Ważne jest jednak pominięcie części, które nie powinny być wyprowadzane; pozostałe części są przypisywane do identyfikatora i wysyłane do użytkownika w formacie zrozumiałym dla JavaScript handler. - -Składnia .[#toc-syntax] ------------------------ +Snippety w Latte .[#toc-snippets-in-latte] +------------------------------------------ -Jeśli wewnątrz szablonu znajduje się kontrolka lub snippet, musimy owinąć go znacznikiem `{snippet} ... {/snippet}` para - zapewniają one wycięcie wyrenderowanego snippetu i wysłanie go do przeglądarki. Zawija go również za pomocą tagu pomocniczego `
    ` z wygenerowanym `id`. W powyższym przykładzie snippet nosi nazwę `header` i może również reprezentować np. szablon kontrolny: +Używanie snippetów w Latte jest niezwykle proste. Aby zdefiniować część szablonu jako snippet, wystarczy zawinąć go w znaczniki `{snippet}` i `{/snippet}`: ```latte {snippet header} @@ -139,7 +133,9 @@ Jeśli wewnątrz szablonu znajduje się kontrolka lub snippet, musimy owinąć g {/snippet} ``` -Fragment o typie innym niż `
    ` lub snippet z dodatkowymi atrybutami HTML uzyskuje się poprzez zastosowanie wariantu atrybutów: +Snippet tworzy element `
    ` na stronie HTML ze specjalnie wygenerowanym `id`. Podczas przerysowywania fragmentu zawartość tego elementu jest aktualizowana. Dlatego też, gdy strona jest początkowo renderowana, wszystkie snippety muszą być również renderowane, nawet jeśli początkowo mogą być puste. + +Można również utworzyć snippet z elementem innym niż `
    ` używając atrybutu n:attribute: ```latte
    @@ -148,138 +144,106 @@ Fragment o typie innym niż `
    ` lub snippet z dodatkowymi atrybutami HTML uz ``` -Dynamiczne fragmenty .[#toc-dynamic-snippets] -============================================= +Obszary wycinków .[#toc-snippet-areas] +-------------------------------------- -Nette pozwala również na stosowanie snippetów, których nazwa jest tworzona w czasie biegu - czyli dynamicznie. Jest to przydatne w przypadku różnych list, gdzie przy zmianie jednego wiersza nie chcemy AJAXować całej listy, a jedynie sam wiersz. Przykład: +Nazwy fragmentów mogą być również wyrażeniami: ```latte -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Mamy tu statyczny snippet `itemsContainer`, zawierający kilka dynamicznych snippetów `item-0`, `item-1` itd. +W ten sposób otrzymamy kilka snippetów, takich jak `item-0`, `item-1`, itd. Gdybyśmy bezpośrednio unieważnili dynamiczny snippet (np. `item-1`), nic nie zostałoby przerysowane. Powodem jest to, że snippety działają jako prawdziwe fragmenty i tylko one są renderowane bezpośrednio. Jednak w szablonie nie ma technicznie snippetu o nazwie `item-1`. Pojawia się on dopiero podczas wykonywania otaczającego kodu snippetu, w tym przypadku pętli foreach. Dlatego część szablonu, która musi zostać wykonana, oznaczymy tagiem `{snippetArea}`: -Nie można bezpośrednio unieważnić dynamicznych snippetów (unieważnienie `item-1` nic nie daje), trzeba unieważnić ich nadrzędny statyczny snippet (tutaj snippet `itemsContainer`). Wówczas cały kod kontenera zostanie wykonany, ale do przeglądarki zostaną wysłane tylko jego podkontenerowe snippety. Jeśli chcesz, aby przeglądarka otrzymała tylko jeden z nich, musisz zmodyfikować dane wejściowe tego kontenera, aby nie generował pozostałych. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -W powyższym przykładzie musisz po prostu upewnić się, że gdy wykonasz żądanie ajaxowe, w zmiennej `$list` znajduje się tylko jeden wpis, a zatem, że pętla `foreach` wypełnia tylko jeden dynamiczny snippet: +I przerysujemy zarówno pojedynczy snippet, jak i cały obszar nadrzędny: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * Tato metoda vrací data pro seznam. - * Obvykle se jedná pouze o vyžádání dat z modelu. - * Pro účely tohoto příkladu jsou data zadána natvrdo. - */ - private function getTheWholeList(): array - { - return [ - 'První', - 'Druhý', - 'Třetí', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +Ważne jest również, aby upewnić się, że tablica `$items` zawiera tylko elementy, które powinny zostać przerysowane. -Snippety w dołączonym szablonie .[#toc-snippets-in-an-included-template] -======================================================================== - -Może się zdarzyć, że w jakimś szablonie mamy snippet, który dopiero chcemy włączyć do innego szablonu. W tym przypadku musimy owinąć osadzenie tego szablonu znacznikami `snippetArea`, które następnie unieważniamy wraz z samym snippetem. - -Znaczniki `snippetArea` gwarantują, że kod osadzający szablon zostanie wykonany, ale do przeglądarki zostanie wysłany tylko wycinek z osadzanego szablonu. +Podczas wstawiania innego szablonu do szablonu głównego za pomocą znacznika `{include}`, który zawiera snippety, konieczne jest ponowne zawinięcie dołączonego szablonu w `snippetArea` i unieważnienie zarówno snippetu, jak i obszaru razem: ```latte -{* parent.latte *} -{snippetArea wrapper} -{include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* included.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -Takie podejście może być również stosowane w połączeniu z dynamicznymi snippetami. +Snippety w komponentach .[#toc-snippets-in-components] +------------------------------------------------------ -Dodawanie i usuwanie .[#toc-adding-and-deleting] -================================================ - -Jeśli dodasz nowy element i unieważnisz `itemsContainer`, to żądanie AJAX zwróci również nowy snippet, ale handler javascript nie może go nigdzie przypisać. W rzeczywistości na stronie nie ma jeszcze elementu HTML o tym identyfikatorze. - -W takim przypadku najłatwiej jest owinąć całą listę jeszcze jednym snippetem i unieważnić całość: +Możesz tworzyć fragmenty w [komponentach |components], a Nette automatycznie je przerysuje. Istnieje jednak pewne ograniczenie: aby przerysować snippety, Nette wywołuje metodę `render()` bez żadnych parametrów. Zatem przekazywanie parametrów w szablonie nie będzie działać: ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Wysyłanie danych użytkownika .[#toc-sending-user-data] +------------------------------------------------------ + +Wraz ze snippetami można wysyłać do klienta dowolne dodatkowe dane. Wystarczy zapisać je w obiekcie `payload`: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + //... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -To samo tyczy się usuwania. Można by jakoś wysłać pusty snippet, ale w praktyce większość list jest paginowana i byłoby to zbyt skomplikowane, aby bardziej ekonomicznie usunąć jeden plus ewentualnie załadować inny (który nie pasował wcześniej). - -Wysyłanie parametrów do komponentu .[#toc-sending-parameters-to-component] -========================================================================== +Wysyłanie parametrów .[#toc-sending-parameters] +=============================================== -Jeśli wysyłamy parametry do komponentu za pomocą żądania AJAX, zarówno parametry sygnałowe, jak i parametry trwałe, musimy określić ich globalną nazwę w żądaniu, które zawiera nazwę komponentu. Pełna nazwa parametru jest zwracana przez metodę `getParameterId()`. +Gdy wysyłamy parametry do komponentu za pośrednictwem żądania AJAX, niezależnie od tego, czy są to parametry sygnału, czy parametry trwałe, musimy podać ich globalną nazwę, która zawiera również nazwę komponentu. Pełną nazwę parametru zwraca metoda `getParameterId()`. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -Metoda handle z odpowiednimi parametrami w komponencie. +Metoda uchwytu z odpowiednimi parametrami w komponencie: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/pl/bootstrap.texy b/application/pl/bootstrap.texy index 3fb6475d75..e7e9905784 100644 --- a/application/pl/bootstrap.texy +++ b/application/pl/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + private Configurator $configurator; + private string $rootDir; + + public function __construct() + { + $this->rootDir = dirname(__DIR__); + // Konfigurator jest odpowiedzialny za konfigurację środowiska aplikacji i usług. + $this->configurator = new Configurator; + // Ustawienie katalogu dla plików tymczasowych generowanych przez Nette (np. skompilowanych szablonów). + $this->configurator->setTempDirectory($this->rootDir . '/temp'); + } + + public function bootWebApplication(): Nette\DI\Container + { + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); + } + + private function initializeEnvironment(): void { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // Nette jest inteligentny i tryb deweloperski włącza się automatycznie, + // lub można go włączyć dla określonego adresu IP, odkomentowując następującą linię: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Włącza Tracy: najlepsze narzędzie do debugowania. + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: automatycznie ładuje wszystkie klasy w podanym katalogu + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Ładowanie plików konfiguracyjnych + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php .[#toc-index-php] =========================== -Podstawowym plikiem w przypadku aplikacji internetowych jest `index.php`, który znajduje się w katalogu publicznym `www/`. Spowoduje to, że klasa Bootstrap zainicjuje środowisko i zwróci `$configurator`, a następnie wyprodukuje kontener DI. Następnie pobiera z niego usługę `Application`, która uruchamia aplikację internetową: +W przypadku aplikacji internetowych, podstawowym plikiem jest `index.php`, który znajduje się w [publicznym katalogu |directory-structure#public-directory-www] `www/`. Klasa Bootstrap inicjalizuje środowisko i tworzy kontener DI. Następnie pobiera z niego usługę `Application`, która uruchamia aplikację internetową: ```php -// inicjalizacja środowiska + uzyskanie obiektu Configurator -$configurator = App\Bootstrap::boot(); -// tworzenie kontenera DI -$container = $configurator->createContainer(); -// Kontener DI tworzy obiekt "Nette +$bootstrap = new App\Bootstrap; +// Inicjalizacja środowiska + utworzenie kontenera DI +$container = $bootstrap->bootWebApplication(); +// Kontener DI tworzy obiekt Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -// uruchomienie aplikacji Nette +// Uruchom aplikację Nette i obsłuż przychodzące żądanie $application->run(); ``` @@ -59,26 +84,42 @@ Jak widać, klasa [api:Nette\Bootstrap\Configurator] pomaga w konfiguracji środ Tryb deweloperski a produkcyjny .[#toc-development-vs-production-mode] ====================================================================== -Nette rozróżnia dwa podstawowe tryby, w których realizowane jest żądanie: deweloperski i produkcyjny. Tryb deweloperski ma na celu maksymalną wygodę dla programisty, wyświetlana jest Tracy, pamięć podręczna jest automatycznie aktualizowana, gdy zmieniają się szablony lub konfiguracje kontenerów DI itp. Produkcja skupia się na wydajności i rześkim wdrożeniu, Tracy tylko loguje błędy, a zmiany w szablonach i innych plikach nie są testowane. +Nette zachowuje się różnie w zależności od tego, czy działa na serwerze deweloperskim czy produkcyjnym: + +🛠️ Tryb deweloperski: + - Wyświetla pasek debugowania Tracy z przydatnymi informacjami (np. zapytania SQL, czas wykonania, użycie pamięci). + - Wyświetla szczegółową stronę błędu ze śladami wywołań funkcji i zawartością zmiennych w przypadku wystąpienia błędu. + - Automatycznie odświeża pamięć podręczną, gdy modyfikowane są szablony Latte, pliki konfiguracyjne itp. + + +Tryb produkcyjny: + - Nie wyświetla żadnych informacji debugowania; wszystkie błędy są rejestrowane. + - Wyświetla stronę `ErrorPresenter` lub ogólną stronę "Błąd serwera", gdy wystąpi błąd. + - Pamięć podręczna nigdy nie jest automatycznie odświeżana! + - Zoptymalizowany pod kątem szybkości i bezpieczeństwa. -Wybór trybu odbywa się poprzez autodetekcję, więc zazwyczaj nie ma potrzeby konfigurowania czy ręcznego przełączania czegokolwiek. Trybem deweloperskim jest sytuacja, kiedy aplikacja jest uruchomiona na localhoście (czyli na adresie IP `127.0.0.1` lub `::1`) i nie ma proxy (czyli jego nagłówka HTTP). W przeciwnym razie działa w trybie produkcyjnym. + +Tryb jest określany automatycznie, więc w większości przypadków nie ma potrzeby konfigurowania lub przełączania go ręcznie: + +- Tryb deweloperski: Aktywny na localhost (adres IP `127.0.0.1` lub `::1`), chyba że używany jest serwer proxy (tj. na podstawie jego nagłówków HTTP). +- Tryb produkcyjny: Aktywny wszędzie indziej. Jeśli chcemy włączyć tryb deweloperski w innych przypadkach, takich jak programiści uzyskujący dostęp z określonego adresu IP, używamy `setDebugMode()`: ```php -$configurator->setDebugMode('23.75.345.200'); // można również określić pole adresu IP +$this->configurator->setDebugMode('23.75.345.200'); // można również określić pole adresu IP ``` Zdecydowanie zalecamy połączenie adresu IP z plikiem cookie. W pliku cookie `nette-debug` przechowujemy tajny token, np. `secret1234`, i w ten sposób umożliwiamy tryb deweloperski dla programistów uzyskujących dostęp z określonego adresu IP i posiadających token w pliku cookie: ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` Możemy również całkowicie wyłączyć tryb deweloperski, nawet dla localhost: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` Uwaga, wartość `true` domyślnie włącza tryb deweloperski, co nigdy nie może mieć miejsca na serwerze produkcyjnym. @@ -90,7 +131,7 @@ Narzędzie do debugowania Tracy .[#toc-debugging-tool-tracy] Aby ułatwić debugowanie, włączmy wspaniałe narzędzie [Tracy |tracy:]. Wizualizuje błędy w trybie deweloperskim i loguje błędy w trybie produkcyjnym do podanego katalogu: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ Pliki tymczasowe .[#toc-temporary-files] Nette używa buforowania dla kontenera DI, RobotLoader, szablonów itp. Dlatego musisz ustawić ścieżkę do katalogu, w którym będzie przechowywany cache: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` W systemach Linux lub macOS ustaw katalogi `log/` i `temp/` na uprawnienia do [zapisu |nette:troubleshooting#Setting-Directory-Permissions]. @@ -112,7 +153,7 @@ RobotLoader .[#toc-robotloader] Zazwyczaj będziemy chcieli automatycznie załadować klasy za pomocą [RobotLoader |robot-loader:], więc musimy go uruchomić i kazać mu załadować klasy z katalogu, w którym znajduje się `Bootstrap.php` (czyli `__DIR__`), oraz z wszelkich podkatalogów: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ Strefa czasowa .[#toc-timezone] Domyślną strefę czasową można ustawić za pośrednictwem konfiguratora. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ W trybie deweloperskim kontener jest automatycznie aktualizowany przy każdej zm Pliki konfiguracyjne są ładowane za pomocą `addConfig()`: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` Jeśli chcemy dodać więcej plików konfiguracyjnych, możemy wywołać funkcję `addConfig()` wielokrotnie. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ Parametry statyczne .[#toc-static-parameters] Parametry wykorzystywane w plikach konfiguracyjnych można zdefiniować [w sekcji `parameters` |dependency-injection:configuration#parameters], a także przekazać (lub nadpisać) za pomocą metody `addStaticParameters()` (posiada ona alias `addParameters()`). Co ważne, różne wartości parametrów spowodują wygenerowanie dodatkowych kontenerów DI, czyli dodatkowych klas. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ Parametry dynamiczne .[#toc-dynamic-parameters] Do kontenera możemy również dodać parametry dynamiczne, których różne wartości, w przeciwieństwie do parametrów statycznych, nie będą powodowały generowania nowych kontenerów DI. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ Możemy po prostu dodać np. zmienne środowiskowe, do których następnie możemy się odwołać w konfiguracji pisząc `%env.variable%`. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ W plikach konfiguracyjnych można używać następujących parametrów statyczny - `%wwwDir%` jest bezwzględną ścieżką do katalogu zawierającego plik wejściowy `index.php` - `%tempDir%` jest bezwzględną ścieżką do katalogu plików tymczasowych - `%vendorDir%` to bezwzględna ścieżka do katalogu, w którym Composer instaluje biblioteki +- `%rootDir%` to bezwzględna ścieżka do katalogu głównego projektu - `%debugMode%` wskazuje, czy aplikacja jest w trybie debugowania - `%consoleMode%` wskazuje, czy żądanie przyszło z linii poleceń @@ -225,7 +268,7 @@ services: A w bootstrapie wstawiamy obiekt do kontenera: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Różne środowiska .[#toc-different-environments] =============================================== -Zapraszamy do dostosowania klasy Bootstrap do swoich potrzeb. Możesz dodać parametry do metody `boot()`, aby odróżnić projekty internetowe, lub dodać inne metody, takie jak `bootForTests()`, która inicjalizuje środowisko dla testów jednostkowych, `bootForCli()` dla skryptów wywoływanych z linii poleceń itp. +Nie wahaj się dostosować klasy `Bootstrap` do swoich potrzeb. Możesz dodać parametry do metody `bootWebApplication()`, aby rozróżnić projekty internetowe. Alternatywnie można dodać inne metody, takie jak `bootTestEnvironment()` do inicjalizacji środowiska dla testów jednostkowych, `bootConsoleApplication()` dla skryptów wywoływanych z wiersza poleceń itp. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container +{ + Tester\Environment::setup(); // Inicjalizacja testera sieci + $this->setupContainer(); + return $this->configurator->createContainer(); +} + +public function bootConsoleApplication(): Nette\DI\Container { - $configurator = self::boot(); - Tester\Environment::setup(); // inicializace Nette Testeru - return $configurator; + $this->configurator->setDebugMode(false); + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); } ``` diff --git a/application/pl/components.texy b/application/pl/components.texy index 9a279ce4c5..21cbd673e9 100644 --- a/application/pl/components.texy +++ b/application/pl/components.texy @@ -230,6 +230,28 @@ Do szablonu wiadomości te są dostępne w zmiennej `$flashes` jako obiekty `std ``` +Przekierowanie po sygnale .[#toc-redirection-after-a-signal] +============================================================ + +Po przetworzeniu sygnału komponentu często następuje przekierowanie. Sytuacja ta jest podobna do formularzy - po przesłaniu formularza również przekierowujemy, aby zapobiec ponownemu przesłaniu danych po odświeżeniu strony w przeglądarce. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Ponieważ komponent jest elementem wielokrotnego użytku i zwykle nie powinien mieć bezpośredniej zależności od konkretnych prezenterów, metody `redirect()` i `link()` automatycznie interpretują parametr jako sygnał komponentu: + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +Jeśli konieczne jest przekierowanie do innego prezentera lub akcji, można to zrobić za pośrednictwem prezentera: + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Trwałe parametry .[#toc-persistent-parameters] ============================================== @@ -347,7 +369,7 @@ services: i w końcu użyć go w naszym prezenterze: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ Komponenty w głębi .[#toc-advanced-use-of-components] Komponenty w aplikacji Nette to części aplikacji internetowej wielokrotnego użytku, które osadzamy w stronach, co jest tematem tego rozdziału. Jakie dokładnie są możliwości takiego komponentu? 1) Jest renderowalny w szablonie -2) wie, która część siebie ma być renderowana podczas [żądania AJAX |ajax#Invalidation] (snippety) +2) wie [, którą część siebie |ajax#snippets] renderować podczas żądania AJAX (snippets) 3) ma możliwość przechowywania swojego stanu w URL (trwałe parametry) 4) posiada zdolność do reagowania na działania (sygnały) użytkownika 5) tworzy strukturę hierarchiczną (gdzie korzeniem jest prezenter) @@ -430,7 +452,7 @@ class PaginatingControl extends Control } ``` -Procesem przeciwnym, czyli pobieraniem wartości z persistent properites, zajmuje się metoda `saveState()`. +Procesem przeciwnym, czyli pobieraniem wartości z persistent properties, zajmuje się metoda `saveState()`. Sygnały w głąb .[#toc-signaly-do-hloubky] diff --git a/application/pl/configuration.texy b/application/pl/configuration.texy index 243df5360c..acfd227530 100644 --- a/application/pl/configuration.texy +++ b/application/pl/configuration.texy @@ -13,11 +13,15 @@ application: # pokazać panel "Nette Application" w Tracy BlueScreen? debugger: ... # (bool) domyślnie jest true - # czy error-presenter zostanie wywołany na błędzie? - catchExceptions: ... # (bool) domyślnie przyjmuje wartość true w trybie produkcyjnym + # czy error-presenter będzie wywoływany przy błędzie? + # działa tylko w trybie deweloperskim + catchExceptions: ... # (bool) domyślnie true # error-presenter name - errorPresenter: Error # (string) domyślnie 'Nette:Error' + errorPresenter: Error # (string|array) domyślnie 'Nette:Error' + + # definiuje aliasy dla prezenterów i wydarzeń + aliases: ... # definiuje zasady rozwiązywania nazwy prezentera do klasy mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) domyślnie jest false ``` -Ponieważ prezentery błędów nie są domyślnie wywoływane w trybie deweloperskim, a błąd jest wyświetlany tylko przez Tracy, zmiana wartości `catchExceptions` na `true` pozwala nam zweryfikować ich poprawną funkcjonalność w trakcie rozwoju. +Od wersji `nette/application` 3.2 możliwe jest zdefiniowanie pary prezenterów błędów: + +```neon +application: + errorPresenter: + 4xx: Error4xx # dla wyjątku Nette\Application\BadRequestException + 5xx: Error5xx # dla innych wyjątków +``` Opcja `silentLinks` określa, jak Nette zachowuje się w trybie rozwoju, gdy generowanie linków nie powiedzie się (np. z powodu braku prezentera itp.). Domyślna wartość `false` oznacza, że Nette rzuci błąd `E_USER_WARNING`. Ustawienie go na `true` spowoduje wyeliminowanie tego komunikatu o błędzie. W środowisku produkcyjnym, `E_USER_WARNING` jest zawsze podniesiony. Na to zachowanie można również wpłynąć poprzez ustawienie zmiennej prezentera [$invalidLinkMode |creating-links#Invalid-Links]. -[Odwzorowanie określa zasady |modules#Mapping], według których nazwa klasy jest wyprowadzana z nazwy prezentera. +[Aliasy ułatwiają odwoływanie się do |creating-links#aliases] często używanych prezenterów. + +[Mapowanie definiuje zasady |directory-structure#Presenter Mapping], według których nazwa klasy pochodzi od nazwy prezentera. Automatyczna rejestracja prezenterów .[#toc-automatic-registration-of-presenters] @@ -82,6 +95,9 @@ latte: # włącza [sprawdzanie wygenerowanego kodu |latte:develop#Checking Generated Code] phpLinter: ... # (string) domyślnie null + # ustawia ustawienia regionalne + locale: cs_CZ # (string) domyślnie null + # klasa obiektów $this->template templateClass: AppMyTemplateClass # domyślnie jest Nette\Bridges\ApplicationLatte\DefaultTemplate ``` @@ -91,7 +107,7 @@ Jeśli używasz Latte w wersji 3, możesz dodać nowe [rozszerzenia |latte:creat ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` /--comment diff --git a/application/pl/creating-links.texy b/application/pl/creating-links.texy index 5c9126dba0..ad83b0e44b 100644 --- a/application/pl/creating-links.texy +++ b/application/pl/creating-links.texy @@ -38,7 +38,7 @@ Możliwe jest również przekazywanie nazwanych parametrów. Poniższy link prze detail produktu ``` -Jeśli metoda `ProductPresenter::renderShow()` nie ma `$lang` w swojej sygnaturze, może pobrać wartość parametru za pomocą `$lang = $this->getParameter('lang')`. +Jeśli metoda `ProductPresenter::renderShow()` nie ma w swojej sygnaturze `$lang`, może pobrać wartość parametru za pomocą `$lang = $this->getParameter('lang')` lub z [właściwości |presenters#Request Parameters]. Jeśli parametry są przechowywane w tablicy, można je rozwinąć za pomocą operatora `...` (w Latte 2.x operator `(expand)`): @@ -103,7 +103,7 @@ Jeśli celem działania jest `default`, możemy go pominąć, ale dwukropek musi úvodní stránka ``` -Linki mogą również wskazywać na inne [moduły |modules]. Tutaj linki są rozróżniane jako względne do zagnieżdżonego podmodułu lub bezwzględne. Zasada działania jest analogiczna do ścieżek dyskowych, ale z dwukropkami zamiast ukośników. Załóżmy, że aktualny prezenter jest częścią modułu `Front`, wtedy piszemy: +Linki mogą również wskazywać na inne [moduły |directory-structure#Presenters and Templates]. W tym przypadku linki dzielą się na względne do podmodułów lub bezwzględne. Zasada jest analogiczna do ścieżek dyskowych, tylko zamiast ukośników są dwukropki. Załóżmy, że rzeczywisty prezenter jest częścią modułu `Front`, wtedy napiszemy: ```latte odkaz na Front:Shop:Product:show @@ -140,7 +140,7 @@ Cel `this` tworzy link do bieżącej strony: refresh ``` -Wszystkie parametry określone w podpisie są również przekazywane `render()` lub `action()` metody. Jeśli więc jesteśmy na `Product:show` i `id: 123`, to link do `this` również przekaże ten parametr. +W tym samym czasie, wszystkie parametry określone w sygnaturze parametru `action()` lub `render()` jeśli metoda `action()` nie są zdefiniowane, są przekazywane. Jeśli więc jesteśmy na stronach `Product:show` i `id:123`, link do `this` również przekaże ten parametr. Oczywiście istnieje możliwość bezpośredniego podania parametrów: @@ -213,7 +213,7 @@ Ponieważ [komponenty |components] są samodzielnymi, wielokrotnego użytku jedn Gdybyśmy chcieli odwołać się do prezenterów w szablonie komponentu, użylibyśmy do tego celu tagu `{plink}`: ```latte -úvod +home ``` lub w kodzie @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Pseudonimy .[#toc-aliases]{data-version:v3.2.2} +=============================================== + +Czasami przydatne jest przypisanie łatwego do zapamiętania aliasu do pary Presenter:action. Na przykład, można nazwać stronę główną `Front:Home:default` po prostu jako `home` lub `Admin:Dashboard:default` jako `admin`. + +Aliasy są definiowane w [konfiguracji |configuration] pod kluczem `application › aliases`: + +```neon +application: + aliases: + home: Front:Home:default + admin: Admin:Dashboard:default + sign: Front:Sign:in +``` + +W linkach są one zapisywane przy użyciu symbolu at, na przykład: + +```latte +administration +``` + +Są one obsługiwane we wszystkich metodach, które działają z linkami, takimi jak `redirect()` i podobne. + + Nieprawidłowe linki .[#toc-invalid-links] ========================================= @@ -257,6 +281,6 @@ Jak tworzyć linki z podobną wygodą jak metoda `link()`, ale bez obecności pr LinkGenerator to usługa, którą można mieć przekazaną przez konstruktor, a następnie tworzyć linki za pomocą jego metody `link()`. -W porównaniu z prezenterami jest różnica. LinkGenerator tworzy wszystkie linki bezpośrednio jako bezwzględne adresy URL. Nie ma również "rzeczywistego prezentera", więc nie możesz po prostu podać nazwy akcji `link('default')` jako celu lub podać względnych ścieżek do [modułów |modules]. +W porównaniu do prezenterów, jest różnica. LinkGenerator tworzy wszystkie linki bezpośrednio jako bezwzględne adresy URL. Ponadto nie ma "rzeczywistego prezentera", więc nie można po prostu podać nazwy akcji `link('default')` jako celu lub podać względnych ścieżek do modułów. Nieważne linki zawsze wyrzucają `Nette\Application\UI\InvalidLinkException`. diff --git a/application/pl/directory-structure.texy b/application/pl/directory-structure.texy new file mode 100644 index 0000000000..28d9a1d58a --- /dev/null +++ b/application/pl/directory-structure.texy @@ -0,0 +1,526 @@ +Struktura katalogów aplikacji +***************************** + +
    + +Jak zaprojektować przejrzystą i skalowalną strukturę katalogów dla projektów w Nette Framework? Pokażemy Ci sprawdzone praktyki, które pomogą Ci zorganizować kod. Dowiesz się: + +- jak **logicznie zorganizować** aplikację w katalogi +- jak zaprojektować strukturę, aby **dobrze się skalowała** wraz z rozwojem projektu +- jakie są **możliwe alternatywy** i ich zalety lub wady + +
    + + +Ważne jest, aby wspomnieć, że sam Nette Framework nie nalega na żadną konkretną strukturę. Został zaprojektowany tak, aby można go było łatwo dostosować do wszelkich potrzeb i preferencji. + + +Podstawowa struktura projektu .[#toc-basic-project-structure] +============================================================= + +Chociaż Nette Framework nie narzuca żadnej stałej struktury katalogów, istnieje sprawdzony domyślny układ w postaci [Web Project |https://github.com/nette/web-project]: + +/--pre +web-project/ +├── app/ ← katalog aplikacji +├── assets/ ← SCSS, pliki JS, obrazy..., alternatywnie resources/ +├── bin/ ← skrypty wiersza poleceń +├── config/ ← konfiguracja +├── log/ ← zarejestrowane błędy +├── temp/ ← pliki tymczasowe, pamięć podręczna +├── tests/ ← testy +├── vendor/ ← biblioteki zainstalowane przez Composer +└── www/ ← katalog publiczny (document-root) +\-- + +Możesz dowolnie modyfikować tę strukturę zgodnie ze swoimi potrzebami - zmieniać nazwy lub przenosić foldery. Następnie wystarczy dostosować względne ścieżki do katalogów w `Bootstrap.php` i ewentualnie `composer.json`. Nic więcej nie jest potrzebne, żadnej skomplikowanej rekonfiguracji, żadnych ciągłych zmian. Nette posiada inteligentne automatyczne wykrywanie i automatycznie rozpoznaje lokalizację aplikacji, w tym jej bazę URL. + + +Zasady organizacji kodu .[#toc-code-organization-principles] +============================================================ + +Kiedy po raz pierwszy odkrywasz nowy projekt, powinieneś być w stanie szybko się zorientować. Wyobraź sobie, że klikasz na katalog `app/Model/` i widzisz następującą strukturę: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +Z tego dowiesz się tylko, że projekt korzysta z niektórych usług, repozytoriów i encji. Nie dowiesz się niczego o rzeczywistym celu aplikacji. + +Przyjrzyjmy się innemu podejściu - **organizacji według domen**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Jest inaczej - na pierwszy rzut oka widać, że jest to witryna e-commerce. Same nazwy katalogów zdradzają, co potrafi aplikacja - działa z płatnościami, zamówieniami i produktami. + +Pierwsze podejście (organizacja według typu klasy) przynosi kilka problemów w praktyce: kod, który jest logicznie powiązany, jest rozproszony w różnych folderach i trzeba między nimi przeskakiwać. Dlatego będziemy organizować według domen. + + +Przestrzenie nazw .[#toc-namespaces] +------------------------------------ + +Konwencjonalne jest, że struktura katalogów odpowiada przestrzeniom nazw w aplikacji. Oznacza to, że fizyczna lokalizacja plików odpowiada ich przestrzeni nazw. Na przykład, klasa znajdująca się w `app/Model/Product/ProductRepository.php` powinna mieć przestrzeń nazw `App\Model\Product`. Zasada ta pomaga w orientacji kodu i upraszcza automatyczne ładowanie. + + +Liczba pojedyncza a mnoga w nazwach .[#toc-singular-vs-plural-in-names] +----------------------------------------------------------------------- + +Zauważ, że używamy liczby pojedynczej dla głównych katalogów aplikacji: `app`, `config`, `log`, `temp`, `www`. To samo dotyczy wewnątrz aplikacji: `Model`, `Core`, `Presentation`. Dzieje się tak, ponieważ każdy z nich reprezentuje jedną ujednoliconą koncepcję. + +Podobnie, `app/Model/Product` reprezentuje wszystko o produktach. Nie nazywamy go `Products`, ponieważ nie jest to folder pełen produktów (który zawierałby pliki takie jak `iphone.php`, `samsung.php`). Jest to przestrzeń nazw zawierająca klasy do pracy z produktami - `ProductRepository.php`, `ProductService.php`. + +Folder `app/Tasks` ma liczbę mnogą, ponieważ zawiera zestaw samodzielnych skryptów wykonywalnych - `CleanupTask.php`, `ImportTask.php`. Każdy z nich jest niezależną jednostką. + +Dla zachowania spójności zalecamy używanie: +- Singular dla przestrzeni nazw reprezentujących jednostkę funkcjonalną (nawet w przypadku pracy z wieloma jednostkami) +- Liczba mnoga dla zbiorów niezależnych jednostek +- W przypadku niepewności lub jeśli nie chcesz o tym myśleć, wybierz liczbę pojedynczą + + +Katalog publiczny `www/` .[#toc-public-directory-www] +===================================================== + +Ten katalog jest jedynym dostępnym z sieci (tzw. document-root). Często można spotkać się z nazwą `public/` zamiast `www/` - jest to tylko kwestia konwencji i nie ma wpływu na funkcjonalność. Katalog zawiera: +- [Punkt wejścia |bootstrap#index.php] aplikacji `index.php` +- Plik `.htaccess` z regułami mod_rewrite (dla Apache) +- Pliki statyczne (CSS, JavaScript, obrazy) +- Przesłane pliki + +Dla prawidłowego bezpieczeństwa aplikacji kluczowe znaczenie ma poprawnie [skonfigurowany document-root |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url]. + +.[note] +Nigdy nie umieszczaj folderu `node_modules/` w tym katalogu - zawiera on tysiące plików, które mogą być wykonywalne i nie powinny być publicznie dostępne. + + +Katalog aplikacji `app/` .[#toc-application-directory-app] +========================================================== + +Jest to główny katalog z kodem aplikacji. Podstawowa struktura: + +/--pre +app/ +├── Core/ ← Infrastruktura ma znaczenie +├── Model/ ← logika biznesowa +├── Presentation/ ← prezentery i szablony +├── Tasks/ ← skrypty poleceń +└── Bootstrap.php ← klasa bootstrap aplikacji +\-- + +`Bootstrap.php` to [klasa startowa aplikacji |bootstrap], która inicjalizuje środowisko, ładuje konfigurację i tworzy kontener DI. + +Przyjrzyjmy się teraz szczegółowo poszczególnym podkatalogom. + + +Prezentery i szablony .[#toc-presenters-and-templates] +====================================================== + +Część prezentacyjna aplikacji znajduje się w katalogu `app/Presentation`. Alternatywą jest krótki `app/UI`. Jest to miejsce dla wszystkich prezenterów, ich szablonów i wszelkich klas pomocniczych. + +Organizujemy tę warstwę według domen. W złożonym projekcie, który łączy e-commerce, blog i API, struktura wyglądałaby następująco: + +/--pre +app/Presentation/ +├── Shop/ ← frontend e-commerce +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← blog +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administracja +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← Punkty końcowe API + └── V1/ +\-- + +I odwrotnie, dla prostego bloga użylibyśmy tej struktury: + +/--pre +app/Presentation/ +├── Front/ ← frontend strony +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administracja +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, mapy witryn itp. +\-- + +Foldery takie jak `Home/` lub `Dashboard/` zawierają prezentery i szablony. Foldery takie jak `Front/`, `Admin/` lub `Api/` nazywane są **modułami**. Z technicznego punktu widzenia są to zwykłe katalogi, które służą do logicznej organizacji aplikacji. + +Każdy folder z prezenterem zawiera podobnie nazwany prezenter i jego szablony. Na przykład folder `Dashboard/` zawiera: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← prezenter +└── default.latte ← szablon +\-- + +Ta struktura katalogów jest odzwierciedlona w przestrzeniach nazw klas. Na przykład `DashboardPresenter` znajduje się w przestrzeni nazw `App\Presentation\Admin\Dashboard` (zobacz [mapowanie prezentera |#presenter mapping]): + +```php +namespace App\Presentation\Admin\Dashboard; + +class DashboardPresenter extends Nette\Application\UI\Presenter +{ + //... +} +``` + +Odnosimy się do prezentera `Dashboard` wewnątrz modułu `Admin` w aplikacji używając notacji dwukropka jako `Admin:Dashboard`. Do jego akcji `default` następnie jako `Admin:Dashboard:default`. W przypadku modułów zagnieżdżonych używamy więcej dwukropków, na przykład `Shop:Order:Detail:default`. + + +Elastyczny rozwój struktury .[#toc-flexible-structure-development] +------------------------------------------------------------------ + +Jedną z największych zalet tej struktury jest to, jak elegancko dostosowuje się ona do rosnących potrzeb projektu. Jako przykład weźmy część generującą kanały XML. Początkowo mamy prosty formularz: + +/--pre +Export/ +├── ExportPresenter.php ← Jeden prezenter dla wszystkich eksportów +├── sitemap.latte ← szablon dla mapy strony +└── feed.latte ← szablon dla kanału RSS +\-- + +Z czasem dodajemy więcej typów feedów i potrzebujemy dla nich więcej logiki... Żaden problem! Folder `Export/` staje się po prostu modułem: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← kanał dla Amazon + └── ebay.latte ← kanał dla eBay +\-- + +Transformacja ta jest całkowicie bezproblemowa - wystarczy utworzyć nowe podfoldery, podzielić na nie kod i zaktualizować odnośniki (np. z `Export:feed` na `Export:Feed:amazon`). Dzięki temu możemy stopniowo rozbudowywać strukturę w miarę potrzeb, poziom zagnieżdżenia nie jest w żaden sposób ograniczony. + +Przykładowo, jeśli w administracji mamy wiele prezenterów związanych z zarządzaniem zamówieniami, takich jak `OrderDetail`, `OrderEdit`, `OrderDispatch` itp. to dla lepszej organizacji możemy utworzyć moduł (folder) `Order`, który będzie zawierał (foldery dla) prezenterów `Detail`, `Edit`, `Dispatch` i innych. + + +Lokalizacja szablonu .[#toc-template-location] +---------------------------------------------- + +W poprzednich przykładach widzieliśmy, że szablony znajdują się bezpośrednio w folderze z prezenterem: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← prezenter +├── DashboardTemplate.php ← opcjonalna klasa szablonu +└── default.latte ← szablon +\-- + +Ta lokalizacja okazuje się najwygodniejsza w praktyce - masz wszystkie powiązane pliki pod ręką. + +Alternatywnie można umieścić szablony w podfolderze `templates/`. Nette obsługuje oba warianty. Można nawet umieścić szablony całkowicie poza folderem `Presentation/`. Wszystko na temat opcji lokalizacji szablonów można znaleźć w rozdziale [Template Lookup |templates#Template Lookup]. + + +Klasy pomocnicze i komponenty .[#toc-helper-classes-and-components] +------------------------------------------------------------------- + +Prezentery i szablony często są dostarczane z innymi plikami pomocniczymi. Umieszczamy je logicznie zgodnie z ich zakresem: + +1. **Bezpośrednio z prezenterem** w przypadku specyficznych komponentów dla danego prezentera: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← komponent dla listy produktów +└── FilterForm.php ← formularz do filtrowania +\-- + +2. **Dla modułu** - zalecamy korzystanie z folderu `Accessory`, który jest umieszczony na początku alfabetu: + +/--pre +Front/ +├── Accessory/ +│ ├── NavbarControl.php ← komponenty dla frontendu +│ └── TemplateFilters.php +├── Product/ +└── Cart/ +\-- + +3. **Dla całej aplikacji** - w folderze `Presentation/Accessory/`: +/--pre +app/Presentation/ +├── Accessory/ +│ ├── LatteExtension.php +│ └── TemplateFilters.php +├── Front/ +└── Admin/ +\-- + +Można też umieścić klasy pomocnicze, takie jak `LatteExtension.php` lub `TemplateFilters.php` w folderze infrastruktury `app/Core/Latte/`. A komponenty w `app/Components`. Wybór zależy od konwencji zespołu. + + +Model - serce aplikacji .[#toc-model-heart-of-the-application] +============================================================== + +Model zawiera całą logikę biznesową aplikacji. Dla jego organizacji obowiązuje ta sama zasada - strukturyzujemy według domen: + +/--pre +app/Model/ +├── Payment/ ← wszystko o płatnościach +│ ├── PaymentFacade.php ← główny punkt wejścia +│ ├── PaymentRepository.php +│ ├── Payment.php ← podmiot +├── Order/ ← wszystko o zamówieniach +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← wszystko o wysyłce +\-- + +W modelu zazwyczaj spotyka się następujące typy klas: + +**Facades**: reprezentują główny punkt wejścia do określonej domeny w aplikacji. Działają jako orkiestrator, który koordynuje współpracę między różnymi usługami w celu wdrożenia kompletnych przypadków użycia (takich jak "utwórz zamówienie" lub "przetwarzaj płatność"). Pod warstwą orkiestracji fasada ukrywa szczegóły implementacji przed resztą aplikacji, zapewniając w ten sposób czysty interfejs do pracy z daną domeną. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // walidacja + // tworzenie zamówień wysyłanie wiadomości + // wysyłanie wiadomości e-mail + // zapis do statystyk + } +} +``` + +**Usługi**: koncentrują się na konkretnych operacjach biznesowych w domenie. W przeciwieństwie do fasad, które orkiestrują całe przypadki użycia, usługa implementuje określoną logikę biznesową (np. obliczenia cen lub przetwarzanie płatności). Usługi są zazwyczaj bezstanowe i mogą być używane przez fasady jako bloki konstrukcyjne dla bardziej złożonych operacji lub bezpośrednio przez inne części aplikacji do prostszych zadań. + +```php +class PricingService +{ + public function calculateTotal(Order $order): Money + { + // kalkulacja ceny + } +} +``` + +**Repozytoria**: obsługują całą komunikację z magazynem danych, zazwyczaj bazą danych. Ich zadaniem jest ładowanie i zapisywanie encji oraz implementacja metod ich wyszukiwania. Repozytorium chroni resztę aplikacji przed szczegółami implementacji bazy danych i zapewnia obiektowy interfejs do pracy z danymi. + +```php +class OrderRepository +{ + public function find(int $id): ?Order + { + } + + public function findByCustomer(int $customerId): array + { + } +} +``` + +**Entities**: obiekty reprezentujące główne koncepcje biznesowe w aplikacji, które mają swoją tożsamość i zmieniają się w czasie. Zazwyczaj są to klasy mapowane do tabel bazy danych przy użyciu ORM (takich jak Nette Database Explorer lub Doctrine). Podmioty mogą zawierać reguły biznesowe dotyczące ich danych i logiki walidacji. + +```php +// Podmiot zmapowany do tabeli zamówień bazy danych +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, + ]); + } +} +``` + +**Obiekty wartości**: niezmienne obiekty reprezentujące wartości bez własnej tożsamości - na przykład kwota pieniędzy lub adres e-mail. Dwie instancje obiektu wartości z tymi samymi wartościami są uważane za identyczne. + + +Kod infrastruktury .[#toc-infrastructure-code] +============================================== + +Folder `Core/` (lub również `Infrastructure/`) jest domem dla technicznych podstaw aplikacji. Kod infrastruktury zazwyczaj zawiera: + +/--pre +app/Core/ +├── Router/ ← routing i zarządzanie adresami URL +│ └── RouterFactory.php +├── Security/ ← uwierzytelnianie i autoryzacja +│ ├── Authenticator.php +│ └── Authorizator.php +├── Logging/ ← rejestrowanie i monitorowanie +│ ├── SentryLogger.php +│ └── FileLogger.php +├── Cache/ ← warstwa buforowania +│ └── FullPageCache.php +└── Integration/ ← integracja z dodatkowymi usługami + ├── Slack/ + └── Stripe/ +\-- + +W przypadku mniejszych projektów płaska struktura jest naturalnie wystarczająca: + +/--pre +Core/ +├── RouterFactory.php +├── Authenticator.php +└── QueueMailer.php +\-- + +To jest kod, który: + +- obsługuje infrastrukturę techniczną (routing, logowanie, buforowanie) +- Integruje usługi zewnętrzne (Sentry, Elasticsearch, Redis) +- Zapewnia podstawowe usługi dla całej aplikacji (poczta, baza danych) +- Jest w większości niezależny od konkretnej domeny - cache lub logger działa tak samo dla e-commerce lub bloga. + +Zastanawiasz się, czy dana klasa należy do tego miejsca, czy do modelu? Kluczową różnicą jest to, że kod w `Core/`: + +- Nie wie nic o domenie (produkty, zamówienia, artykuły) +- Zazwyczaj może być przeniesiony do innego projektu +- Rozwiązuje "jak to działa" (jak wysłać pocztę), a nie "co to robi" (jaką pocztę wysłać) + +Przykład dla lepszego zrozumienia: + +- `App\Core\MailerFactory` - tworzy instancje klasy wysyłającej e-maile, obsługuje ustawienia SMTP +- `App\Model\OrderMailer` - używa `MailerFactory` do wysyłania e-maili o zamówieniach, zna ich szablony i wie, kiedy powinny zostać wysłane. + + +Skrypty poleceń .[#toc-command-scripts] +======================================= + +Aplikacje często muszą wykonywać zadania poza zwykłymi żądaniami HTTP - niezależnie od tego, czy jest to przetwarzanie danych w tle, konserwacja czy zadania okresowe. Proste skrypty w katalogu `bin/` są używane do wykonania, podczas gdy rzeczywista logika implementacji jest umieszczona w `app/Tasks/` (lub `app/Commands/`). + +Przykład: + +/--pre +app/Tasks/ +├── Maintenance/ ← skrypty konserwacyjne +│ ├── CleanupCommand.php ← usuwanie starych danych +│ └── DbOptimizeCommand.php ← optymalizacja bazy danych +├── Integration/ ← integracja z systemami zewnętrznymi +│ ├── ImportProducts.php ← import z systemu dostawcy +│ └── SyncOrders.php ← synchronizacja zamówień +└── Scheduled/ ← regularne zadania + ├── NewsletterCommand.php ← wysyłanie newsletterów + └── ReminderCommand.php ← powiadomienia dla klientów +\-- + +Co należy do modelu, a co do skryptów poleceń? Na przykład logika wysyłania jednej wiadomości e-mail jest częścią modelu, masowe wysyłanie tysięcy wiadomości e-mail należy do `Tasks/`. + +Zadania są zwykle uruchamiane [z wiersza poleceń |https://blog.nette.org/en/cli-scripts-in-nette-application] lub przez cron. Mogą być również uruchamiane za pośrednictwem żądania HTTP, ale należy wziąć pod uwagę bezpieczeństwo. Prezenter, który uruchamia zadanie, musi być zabezpieczony, na przykład tylko dla zalogowanych użytkowników lub z silnym tokenem i dostępem z dozwolonych adresów IP. W przypadku długich zadań konieczne jest zwiększenie limitu czasu skryptu i użycie `session_write_close()`, aby uniknąć blokowania sesji. + + +Inne możliwe katalogi .[#toc-other-possible-directories] +======================================================== + +Oprócz wspomnianych podstawowych katalogów, można dodać inne wyspecjalizowane foldery zgodnie z potrzebami projektu. Przyjrzyjmy się najpopularniejszym z nich i ich zastosowaniu: + +/--pre +app/ +├── Api/ ← Logika API niezależna od warstwy prezentacji +├── Database/ ← skrypty migracyjne i siewniki danych testowych +├── Components/ ← współdzielone komponenty wizualne w całej aplikacji +├── Event/ ← przydatne w przypadku korzystania z architektury sterowanej zdarzeniami +├── Mail/ ← szablony wiadomości e-mail i powiązana logika +└── Utils/ ← klasy pomocnicze +\-- + +W przypadku współdzielonych komponentów wizualnych używanych w prezenterach w całej aplikacji można użyć folderu `app/Components` lub `app/Controls`: + +/--pre +app/Components/ +├── Form/ ← współdzielone komponenty formularzy +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← komponenty dla list danych +│ └── DataGrid.php +└── Navigation/ ← elementy nawigacyjne + ├── Breadcrumbs.php + └── Menu.php +\-- + +To tutaj znajdują się komponenty z bardziej złożoną logiką. Jeśli chcesz współdzielić komponenty między wieloma projektami, dobrze jest oddzielić je w samodzielnym pakiecie kompozytora. + +W katalogu `app/Mail` można umieścić zarządzanie komunikacją e-mail: + +/--pre +app/Mail/ +├── templates/ ← szablony wiadomości e-mail +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Mapowanie prezentera .[#toc-presenter-mapping] +============================================== + +Mapowanie definiuje zasady wyprowadzania nazw klas z nazw prezenterów. Określamy je w [konfiguracji |configuration] pod kluczem `application › mapping`. + +Na tej stronie pokazaliśmy, że umieszczamy prezenterów w folderze `app/Presentation` (lub `app/UI`). Musimy poinformować Nette o tej konwencji w pliku konfiguracyjnym. Wystarczy jedna linijka: + +```neon +application: + mapping: App\Presentation\*\**Presenter +``` + +Jak działa mapowanie? Aby lepiej to zrozumieć, wyobraźmy sobie najpierw aplikację bez modułów. Chcemy, aby klasy prezenterów należały do przestrzeni nazw `App\Presentation`, tak aby prezenter `Home` był mapowany na klasę `App\Presentation\HomePresenter`. Można to osiągnąć za pomocą tej konfiguracji: + +```neon +application: + mapping: App\Presentation\*Presenter +``` + +Mapowanie działa poprzez zastąpienie gwiazdki w masce `App\Presentation\*Presenter` nazwą prezentera `Home`, w wyniku czego otrzymujemy ostateczną nazwę klasy `App\Presentation\HomePresenter`. Proste! + +Jednakże, jak widać w przykładach w tym i innych rozdziałach, umieszczamy klasy prezenterów w podkatalogach o tej samej nazwie, na przykład prezenter `Home` mapuje się na klasę `App\Presentation\Home\HomePresenter`. Osiągamy to poprzez podwojenie dwukropka (wymaga Nette Application 3.2): + +```neon +application: + mapping: App\Presentation\**Presenter +``` + +Teraz przejdziemy do mapowania prezenterów na moduły. Możemy zdefiniować specyficzne mapowanie dla każdego modułu: + +```neon +application: + mapping: + Front: App\Presentation\Front\**Presenter + Admin: App\Presentation\Admin\**Presenter + Api: App\Api\*Presenter +``` + +Zgodnie z tą konfiguracją, prezenter `Front:Home` mapuje do klasy `App\Presentation\Front\Home\HomePresenter`, podczas gdy prezenter `Api:OAuth` mapuje do klasy `App\Api\OAuthPresenter`. + +Ponieważ moduły `Front` i `Admin` mają podobną metodę mapowania i prawdopodobnie będzie więcej takich modułów, można utworzyć ogólną regułę, która je zastąpi. Nowa gwiazdka dla modułu zostanie dodana do maski klasy: + +```neon +application: + mapping: + *: App\Presentation\*\**Presenter + Api: App\Api\*Presenter +``` + +Działa to również w przypadku głębiej zagnieżdżonych struktur katalogów, takich jak prezenter `Admin:User:Edit`, gdzie segment z gwiazdką powtarza się dla każdego poziomu i skutkuje klasą `App\Presentation\Admin\User\Edit\EditPresenter`. + +Alternatywnym zapisem jest użycie tablicy składającej się z trzech segmentów zamiast ciągu znaków. Ten zapis jest równoważny poprzedniemu: + +```neon +application: + mapping: + *: [App\Presentation, *, **Presenter] + Api: [App\Api, '', *Presenter] +``` diff --git a/application/pl/how-it-works.texy b/application/pl/how-it-works.texy index fa65218c64..000eb37da9 100644 --- a/application/pl/how-it-works.texy +++ b/application/pl/how-it-works.texy @@ -22,18 +22,18 @@ Struktura katalogów wygląda mniej więcej tak: /--pre web-project/ ├── app/ ← adresář s aplikací -│ ├── Presenters/ ← presentery a šablony -│ │ ├── HomePresenter.php ← třída presenteru Home -│ │ └── templates/ ← adresář se šablonami -│ │ ├── @layout.latte ← šablona layoutu -│ │ └── Home/ ← šablony presenteru Home -│ │ └── default.latte ← šablona akce 'default' -│ ├── Router/ ← konfigurace URL adres +│ ├── Core/ ← podstawowe niezbędne klasy +│ │ └── RouterFactory.php ← konfiguracja adresów URL +│ ├── Presentation/ ← prezentery, szablony & co. +│ │ ├── @layout.latte ← szablon udostępnionego layoutu +│ │ └── Home/ ← Katalog główny prezentera +│ │ ├── HomePresenter.php ← Klasa prezentera strony głównej +│ │ └── default.latte ← szablon dla akcji default │ └── Bootstrap.php ← zaváděcí třída Bootstrap ├── bin/ ← skripty spouštěné z příkazové řádky ├── config/ ← konfigurační soubory │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← logované chyby ├── temp/ ← dočasné soubory, cache, … ├── vendor/ ← knihovny instalované Composerem @@ -45,9 +45,9 @@ Struktura katalogów wygląda mniej więcej tak: └── .htaccess ← zakazuje přístup do všech adresářů krom www \-- -Możesz zmienić strukturę katalogów w dowolny sposób, zmienić nazwę lub przenieść foldery, a następnie po prostu edytować ścieżki do `log/` i `temp/` w pliku `Bootstrap.php`, a następnie ścieżkę do tego pliku w `composer.json` w sekcji `autoload`. Nic więcej, żadnych skomplikowanych rekonfiguracji, żadnych zmian w stałych. Nette posiada [sprytną |bootstrap#Development-vs-Production-Mode] funkcję [autodetekcji |bootstrap#Development-vs-Production-Mode]. +Możesz modyfikować strukturę katalogów w dowolny sposób, zmieniać nazwy lub przenosić foldery - jest to całkowicie elastyczne. Nette oferuje również inteligentne automatyczne wykrywanie i automatycznie rozpoznaje lokalizację aplikacji, w tym jej bazę adresów URL. -W przypadku nieco większych aplikacji możemy podzielić foldery prezentera i szablonu na dysku na podkatalogi, a klasy na przestrzenie nazw, które nazywamy [modułami |modules]. +W przypadku nieco większych aplikacji możemy zorganizować foldery prezenterów i szablonów w [podkatalogi |directory-structure#Presenters and templates] i pogrupować klasy w przestrzenie nazw, które nazywamy modułami. Katalog `www/` jest tzw. katalogiem publicznym lub document-root projektu. Możesz zmienić jego nazwę bez konieczności ustawiania czegokolwiek innego po stronie aplikacji. Wystarczy [skonfigurować hosting |nette:troubleshooting#How-to-change-or-remove-www-directory-from-URL] tak, aby document-root trafił do tego katalogu. @@ -75,7 +75,7 @@ Jego celem jest: Jaka fabryka? Nie robimy traktorów, robimy strony internetowe! Trzymaj się, za chwilę się to wyjaśni. -Przez "inicjalizację środowiska" rozumiemy na przykład, że włączona jest [Tracy |tracy:], która jest niesamowitym narzędziem do logowania lub wizualizacji błędów. Rejestruje błędy na serwerze produkcyjnym, a wyświetla je na serwerze deweloperskim. Tak więc inicjalizacja obejmuje podjęcie decyzji, czy witryna działa w trybie produkcyjnym czy rozwojowym. Aby to zrobić, Nette używa autodetekcji: jeśli uruchamiasz stronę na localhost, działa ona w trybie deweloperskim. Nie musisz niczego konfigurować, a aplikacja jest gotowa zarówno do rozwoju, jak i wdrożenia na żywo. Czynności te są wykonywane i szczegółowo opisane w rozdziale poświęconym [klasie Bootstrap |bootstrap]. +Przez "inicjalizację środowiska" rozumiemy na przykład aktywację [Tracy |tracy:], które jest fantastycznym narzędziem do logowania i wizualizacji błędów. Na serwerach produkcyjnych rejestruje błędy, podczas gdy na deweloperskich wyświetla je bezpośrednio. Dlatego inicjalizacja obejmuje określenie, czy witryna działa w trybie produkcyjnym czy deweloperskim. W tym celu Nette wykorzystuje [inteligentne automatyczne wykrywanie |bootstrap#development-vs-production-mode]: jeśli uruchamiasz witrynę na localhost, działa ona w trybie deweloperskim. Nie jest wymagana żadna konfiguracja, a aplikacja jest gotowa zarówno do wdrożenia deweloperskiego, jak i produkcyjnego. Kroki te są wykonywane i szczegółowo opisane w rozdziale dotyczącym [klasy Bootstrap |bootstrap]. Trzeci punkt (tak, pominęliśmy drugi, ale do niego wrócimy) to uruchomienie aplikacji. Obsługą żądań HTTP zajmuje się w Nette klasa `Nette\Application\Application` (dalej `Application`), więc mówiąc o uruchomieniu aplikacji, mamy na myśli w szczególności wywołanie metody o odpowiedniej nazwie `run()` na obiekcie tej klasy. @@ -91,7 +91,7 @@ Aplikacje napisane w Nette są podzielone na wiele prezenterów (w innych framew Aplikacja rozpoczyna się od zapytania tzw. routera o decyzję, do którego prezentera przekazać bieżące żądanie do przetworzenia. Router decyduje o tym, czyja to odpowiedzialność. Patrzy na wejściowy URL `https://example.com/product/123`, którego poprosi o wyświetlenie (`show`) produktu z `id: 123` jako **akcji** . Dobrą praktyką jest zapisanie pary prezenter + akcja oddzielonej dwukropkiem jako `Product:show`. -W ten sposób router przekształca adres URL w parę `Presenter:action` + parametry, w naszym przypadku `Product:show` + `id: 123`. Jak wygląda taki router, można zobaczyć w pliku `app/Router/RouterFactory.php`, a szczegółowo opisujemy go w rozdziale [Routing |Routing]. +W ten sposób router przekształca adres URL w parę `Presenter:action` + parametry, w naszym przypadku `Product:show` + `id: 123`. Jak wygląda taki router, można zobaczyć w pliku `app/Core/RouterFactory.php`, a szczegółowo opisujemy go w rozdziale [Routing |Routing]. Ruszajmy dalej. Aplikacja zna teraz nazwę prezentera i może przejść dalej. Produkując obiekt klasy `ProductPresenter`, który jest kodem prezentera `Product`. Dokładniej, prosi kontener DI o wyprodukowanie prezentera, ponieważ jest tam, aby go wyprodukować. @@ -121,12 +121,9 @@ Wywołana jest więc metoda `renderShow(123)`, której kod jest fikcyjnym przyk Następnie prezenter zwraca odpowiedź. Może to być strona HTML, obraz, dokument XML, wysłanie pliku z dysku, JSON, a nawet przekierowanie na inną stronę. Co ważne, o ile nie powiemy wprost prezenterowi, jak ma odpowiedzieć (co ma miejsce w przypadku `ProductPresenter`), odpowiedzią będzie renderowanie szablonu ze stroną HTML. Dlaczego? Ponieważ w 99% przypadków chcemy renderować szablon, więc prezenter przyjmuje to zachowanie jako domyślne i chce ułatwić nam pracę. O to właśnie chodzi w Nette. -Nie musimy nawet określać, jaki szablon ma być renderowany, sam wywnioskuje ścieżkę do niego za pomocą prostej logiki. W przypadku prezentera `Product` i akcji `show`, spróbuje sprawdzić, czy istnieje jeden z tych plików szablonów przechowywanych względnie z katalogu klasy `ProductPresenter`: +Nie musimy nawet określać, który szablon ma być renderowany; framework sam wydedukuje ścieżkę. W przypadku akcji `show`, po prostu próbuje załadować szablon `show.latte` w katalogu z klasą `ProductPresenter`. Próbuje również znaleźć układ w pliku `@layout.latte` (więcej o [wyszukiwaniu szablonów |templates#Template Lookup]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -Spróbuje również prześledzić układ w pliku `@layout.latte`, a następnie wyrenderować szablon. W ten sposób kończy się zadanie prezentera i aplikacji, a praca zostaje zakończona. Jeśli szablon nie istnieje, zwrócona zostanie strona błędu 404. Więcej o prezenterach można przeczytać na stronie [Prezenterzy |presenters]. +Następnie szablony są renderowane. To kończy zadanie prezentera i całej aplikacji, a praca jest wykonywana. Jeśli szablon nie istnieje, zostanie zwrócona strona błędu 404. Więcej informacji na temat prezenterów można znaleźć na stronie [Prezenterzy |presenters]. [* request-flow.svg *] @@ -137,7 +134,7 @@ Aby być bezpiecznym, spróbujmy podsumować cały proces z nieco innym adresem 3) router dekoduje adres URL jako parę `Home:default` 4) tworzony jest obiekt klasy `HomePresenter` 5) wywoływana jest metoda `renderDefault()` (jeśli istnieje) -6) wyrenderować szablon np. `templates/Home/default.latte` z układem np. `templates/@layout.latte` +6) wyrenderować szablon np. `default.latte` z układem np. `@layout.latte` Teraz być może spotkałeś się z wieloma nowymi pojęciami, ale wierzymy, że mają one sens. Tworzenie aplikacji w Nette to ogromna bułka z masłem. diff --git a/application/pl/modules.texy b/application/pl/modules.texy deleted file mode 100644 index a2a4480132..0000000000 --- a/application/pl/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Moduły -****** - -.[perex] -W Nette moduły reprezentują logiczne jednostki, które tworzą aplikację. Należą do nich prezentery, szablony, ewentualnie komponenty i klasy modeli. - -Jeden komponent dla prezenterów i jeden dla szablonów nie wystarczyłby dla prawdziwych projektów. Posiadanie kilkudziesięciu plików w jednym folderze jest co najmniej niezorganizowane. Jak się z niego wydostać? Po prostu dzielimy je na podkatalogi na dysku i na przestrzenie nazw w kodzie. I właśnie to robią moduły Nette. - -Zapomnijmy więc o jednym folderze dla prezenterów i szablonów, a zamiast tego stwórzmy moduły, na przykład `Admin` i `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← adresář s moduly -│ ├── Admin/ ← modul Admin -│ │ ├── Presenters/ ← jeho presentery -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← modul Front -│ └── Presenters/ ← jeho presentery -│ └── ... -\-- - -Ta struktura katalogów będzie odzwierciedlona w przestrzeni nazw klas, więc na przykład `DashboardPresenter` będzie w przestrzeni `App\Modules\Admin\Presenters`: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Prezenter `Dashboard` wewnątrz modułu `Admin` jest określany w aplikacji za pomocą notacji z podwójną kropką jako `Admin:Dashboard`, a jego działanie `default` jest określane jako `Admin:Dashboard:default`. -A skąd Nette own wie, że `Admin:Dashboard` reprezentuje klasę `App\Modules\Admin\Presenters\DashboardPresenter`? Mówimy to za pomocą [mapowania |#Mappings] w konfiguracji. -Tak więc podana struktura nie jest stała i można ją modyfikować według potrzeb. - -Moduły mogą oczywiście zawierać wszystkie inne części oprócz prezenterów i szablonów, jak komponenty, klasy modeli itp. - - -Moduły zagnieżdżone .[#toc-nested-modules] ------------------------------------------- - -Moduły nie muszą tworzyć tylko płaskiej struktury, można też tworzyć np. submoduły: - -/--pre -app/ -├── Modules/ ← adresář s moduly -│ ├── Blog/ ← modul Blog -│ │ ├── Admin/ ← submodul Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← submodul Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← modul Forum -│ │ └── ... -\-- - -Tak więc moduł `Blog` jest podzielony na podmoduły `Admin` i `Front`. I znowu będzie to odzwierciedlone w przestrzeni nazw, która będzie `App\Modules\Blog\Admin\Presenters` itd. Prezenter `Dashboard` wewnątrz submodułu jest określany jako `Blog:Admin:Dashboard`. - -Rozgałęzienie może iść tak głęboko, jak chcesz, więc możesz tworzyć podmoduły. - - -Tworzenie linków .[#toc-creating-links] ---------------------------------------- - -Linki w szablonach prezenterów są względne do bieżącego modułu. Tak więc link `Foo:default` prowadzi do prezentera `Foo` w tym samym module co aktualny prezenter. Na przykład, jeśli bieżący moduł to `Front`, to link idzie tak: - -```latte -odkaz na Front:Product:show -``` - -Link jest względny, nawet jeśli nazwa modułu jest jego częścią, jest wtedy uważany za submoduł: - -```latte -odkaz na Front:Shop:Product:show -``` - -Odwołania bezwzględne są zapisywane analogicznie do ścieżek bezwzględnych na dysku, ale z dwukropkami zamiast ukośników. Tak więc link bezwzględny zaczyna się od dwukropka: - -```latte -odkaz na Admin:Product:show -``` - -Aby dowiedzieć się, czy jesteśmy w danym module lub submodule, korzystamy z funkcji `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Routing .[#toc-routing] ------------------------ - -Patrz [rozdział dotyczący routingu |routing#Modules]. - - -Mapowanie .[#toc-mapping] -------------------------- - -Określa zasady, według których nazwa klasy jest wyprowadzana z nazwy prezentera. Zapisujemy je w [konfiguracji |configuration] pod kluczem `application › mapping`. - -Zacznijmy od próbki, która nie korzysta z modułów. Będziemy chcieli, aby klasy prezentera miały przestrzeń nazw `App\Presenters`. To znaczy, będziemy chcieli, aby prezenter, na przykład, `Home` mapował do klasy `App\Presenters\HomePresenter`. Można to osiągnąć dzięki następującej konfiguracji: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -Zastąp nazwę prezentera gwiazdką w masce klasy, a wynikiem będzie nazwa klasy. Spokojnie! - -Jeśli złamiemy prezenterów na moduły, możemy mieć niestandardowe mapowanie dla każdego modułu: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Teraz prezenter `Front:Home` mapuje do klasy `App\Modules\Front\Presenters\HomePresenter`, a prezenter `Admin:Dashboard` mapuje do klasy `App\Modules\Admin\Presenters\DashboardPresenter`. - -Bardziej praktyczne będzie stworzenie ogólnej (gwiazdkowej) reguły, która zastąpi pierwsze dwie. W masce klasy zostanie dodana dodatkowa gwiazdka tylko dla tego modułu: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Ale co w przypadku, gdy korzystamy z wielu zagnieżdżonych modułów i mamy np. prezentera `Admin:User:Edit`? W takim przypadku segment z gwiazdką reprezentujący moduł dla każdego poziomu zostanie po prostu powtórzony, a wynikiem będzie klasa `App\Modules\Admin\User\Presenters\EditPresenter`. - -Alternatywną notacją jest użycie tablicy składającej się z trzech segmentów zamiast ciągu. Ta notacja jest równoważna z poprzednią: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -Wartość domyślna to `*: *Module\*Presenter`. diff --git a/application/pl/presenters.texy b/application/pl/presenters.texy index b6a00e6d74..f35bddf004 100644 --- a/application/pl/presenters.texy +++ b/application/pl/presenters.texy @@ -60,7 +60,7 @@ Podobna metoda `render()`. Podczas gdy `render()` Metoda ma na celu Ważne jest to, że `action()` jest wywoływany przed `render()`więc możemy w nim potencjalnie zmienić dalszy bieg historii, czyli zmienić szablon do wylosowania, a także metodę `render()`który zostanie wywołany. Odbywa się to za pomocą strony `setView('jineView')`. -Do metody przekazywane są parametry z żądania. Możliwe i zalecane jest podawanie typów do parametrów, np. `actionShow(int $id, string $slug = null)` - jeśli w parametrze `id` zabraknie lub nie będzie on liczbą całkowitą, prezenter zwróci [błąd 404 |#Error-404-etc] i wyjdzie. +Do metody przekazywane są parametry z żądania. Możliwe i zalecane jest podawanie typów do parametrów, np. `actionShow(int $id, ?string $slug = null)` - jeśli w parametrze `id` zabraknie lub nie będzie on liczbą całkowitą, prezenter zwróci [błąd 404 |#Error-404-etc] i wyjdzie. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ Wiadomości te są dostępne dla szablonu w zmiennej `$flashes` jako obiekty `st Error 404 i co. .[#toc-error-404-etc] ===================================== -Jeśli żądanie nie może zostać spełnione, na przykład dlatego, że artykuł, który chcemy wyświetlić, nie istnieje w bazie danych, rzucamy błąd 404 za pomocą metody `error(string $message = null, int $httpCode = 404)`. +Jeśli żądanie nie może zostać spełnione, na przykład dlatego, że artykuł, który chcemy wyświetlić, nie istnieje w bazie danych, rzucamy błąd 404 za pomocą metody `error(?string $message = null, int $httpCode = 404)`. ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Parametry żądania .[#toc-request-parameters] +============================================ + +Prezenter, jak również każdy komponent, uzyskuje swoje parametry z żądania HTTP. Ich wartości można pobrać za pomocą metody `getParameter($name)` lub `getParameters()`. Wartości są ciągami lub tablicami ciągów, zasadniczo surowymi danymi uzyskanymi bezpośrednio z adresu URL. + +Dla większej wygody zalecamy udostępnianie parametrów za pośrednictwem właściwości. Wystarczy opatrzyć je adnotacją `#[Parameter]` atrybut: + +```php +use Nette\Application\Attributes\Parameter; // Ta linia jest ważna + +class HomePresenter extends Nette\Application\UI\Presenter +{ + #[Parameter] + public string $theme; // musi być publiczny +} +``` + +W przypadku właściwości sugerujemy określenie typu danych (np. `string`). Nette automatycznie rzuci wartość na jej podstawie. Wartości parametrów mogą być również [walidowane |#Validation of Parameters]. + +Podczas tworzenia linku można bezpośrednio ustawić wartość parametrów: + +```latte +click +``` + + Trwałe parametry .[#toc-persistent-parameters] ============================================== @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Jeśli `$this->lang` ma wartość taką jak `'en'`, to linki utworzone przy użyciu `link()` lub `n:href` będą zawierały również parametr `lang=en`. A gdy link zostanie kliknięty, ponownie będzie to `$this->lang = 'en'`. -Dla właściwości zalecamy dołączenie typu danych (np. `string`) i można również dołączyć wartość domyślną. Wartości parametrów mogą być [walidowane |#Validation of Persistent Parameters]. +W przypadku właściwości zalecamy podanie typu danych (np. `string`), a także wartości domyślnej. Wartości parametrów mogą być [walidowane |#Validation of Parameters]. Trwałe parametry są domyślnie przekazywane pomiędzy wszystkimi akcjami danego prezentera. Aby przekazać je pomiędzy wieloma prezenterami, musisz je zdefiniować: @@ -307,18 +333,12 @@ Sięgając głębiej .[#toc-going-deeper] Z tym, co do tej pory omówiliśmy w tym rozdziale, jesteś prawdopodobnie całkowicie zadowolony. Kolejne wiersze są dla tych, którzy interesują się prezenterami dogłębnie i chcą wiedzieć wszystko. -Wymagania i parametry .[#toc-requirement-and-parameters] --------------------------------------------------------- +Walidacja parametrów .[#toc-validation-of-parameters] +----------------------------------------------------- -Żądanie obsługiwane przez prezentera ma postać obiektu [api:Nette\Application\Request] i jest zwracane przez metodę prezentera `getRequest()`. Zawiera ona tablicę parametrów, a każdy z nich należy albo do któregoś z komponentów, albo bezpośrednio do prezentera (który w rzeczywistości też jest komponentem, choć specjalnym). Nette dokonuje więc redystrybucji parametrów i przechodzi między poszczególnymi komponentami (i prezenterem), wywołując metodę `loadState(array $params)`. Parametry można uzyskać za pomocą metody `getParameters(): array`, indywidualnie za pomocą `getParameter($name)`. Wartości parametrów są ciągami lub tablicami ciągów, są to w zasadzie surowe dane uzyskane bezpośrednio z adresu URL. +Wartości [parametrów żądania |#request parameters] i [parametrów stałych |#persistent parameters] otrzymanych z adresów URL są zapisywane we właściwościach przez metodę `loadState()`. Sprawdza również, czy typ danych określony we właściwości jest zgodny, w przeciwnym razie odpowie błędem 404, a strona nie zostanie wyświetlona. - -Walidacja trwałych parametrów .[#toc-validation-of-persistent-parameters] -------------------------------------------------------------------------- - -Wartości [trwałych parametrów |#persistent parameters] otrzymanych z adresów URL są zapisywane do właściwości przez metodę `loadState()`. Sprawdza ona również, czy typ danych określony we właściwości pasuje, w przeciwnym razie odpowie błędem 404 i strona nie zostanie wyświetlona. - -Nigdy ślepo nie ufaj trwałym parametrom, ponieważ mogą one zostać łatwo nadpisane przez użytkownika w adresie URL. Na przykład w ten sposób sprawdzamy, czy `$this->lang` jest wśród obsługiwanych języków. Dobrym sposobem na to jest nadpisanie metody `loadState()` wspomnianej powyżej: +Nigdy nie należy ślepo ufać parametrom, ponieważ mogą one zostać łatwo nadpisane przez użytkownika w adresie URL. Na przykład w ten sposób sprawdzamy, czy `$this->lang` należy do obsługiwanych języków. Dobrym sposobem na to jest nadpisanie metody `loadState()` wspomnianej powyżej: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,7 +361,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Zapisywanie i przywracanie wniosku .[#toc-save-and-restore-the-request] ----------------------------------------------------------------------- -Bieżące żądanie można zapisać do sesji lub przywrócić z niej i pozwolić prezenterowi wykonać je ponownie. Jest to przydatne, na przykład, gdy użytkownik wypełnia formularz i jego login wygasa. Aby uniknąć utraty danych, przed przekierowaniem na stronę logowania zapisujemy bieżące żądanie do sesji za pomocą `$reqId = $this->storeRequest()`, która zwraca jej identyfikator jako krótki łańcuch i przekazuje go jako parametr do prezentera logowania. +Żądanie obsługiwane przez prezentera jest obiektem [api:Nette\Application\Request] i jest zwracane przez metodę prezentera `getRequest()`. + +Możesz zapisać bieżące żądanie w sesji lub przywrócić je z sesji i pozwolić prezenterowi wykonać je ponownie. Jest to przydatne na przykład, gdy użytkownik wypełnia formularz, a jego login wygasa. Aby nie utracić danych, przed przekierowaniem na stronę logowania zapisujemy bieżące żądanie do sesji za pomocą `$reqId = $this->storeRequest()`, która zwraca identyfikator w postaci krótkiego ciągu znaków i przekazuje go jako parametr do prezentera logowania. Po zalogowaniu się wywołujemy metodę `$this->restoreRequest($reqId)`, która pobiera żądanie z sesji i przekazuje je dalej. Ta metoda sprawdza, czy żądanie zostało utworzone przez tego samego użytkownika, który jest teraz zalogowany. Jeśli zalogował się inny użytkownik lub klucz był nieważny, nie robi nic i program kontynuuje. @@ -362,7 +384,7 @@ Przekierowanie nie nastąpi w przypadku żądań AJAX lub POST, ponieważ spowod Możesz również wywołać kanonizację ręcznie za pomocą metody `canonicalize()`, która przekaże prezentera, akcję i parametry podobnie jak w przypadku metody `link()`. Tworzy link i porównuje go z bieżącym adresem URL. Jeśli się różni, przekierowuje na wygenerowany link. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // přesměruje, pokud $slug se liší od $realSlug @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Ograniczenie dostępu przy użyciu `#[Requires]` .[#toc-access-restriction-using-requires]{data-version:3.2.2} +------------------------------------------------------------------------------------------------------------ + +Atrybut `#[Requires]` zapewnia zaawansowane opcje ograniczania dostępu do prezenterów i ich metod. Można go użyć do określenia metod HTTP, wymagania żądań AJAX, ograniczenia dostępu do tego samego źródła i ograniczenia dostępu tylko do przekazywania. Atrybut może być stosowany do klas prezenterów, jak również poszczególnych metod, takich jak `action()`, `render()`, `handle()`, i `createComponent()`. + +Można określić te ograniczenia: +- na metodach HTTP: `#[Requires(methods: ['GET', 'POST'])]` +- wymagające żądania AJAX: `#[Requires(ajax: true)]` +- dostęp tylko z tego samego źródła: `#[Requires(sameOrigin: true)]` +- dostęp tylko przez przekierowanie: `#[Requires(forward: true)]` +- ograniczenia dotyczące określonych działań: `#[Requires(actions: 'default')]` + +Aby uzyskać szczegółowe informacje, zobacz [Jak używać atrybutu Requires atrybut |best-practices:attribute-requires]. + + +Sprawdzanie metod HTTP .[#toc-http-method-check] +------------------------------------------------ + +W Nette prezenterzy automatycznie weryfikują metodę HTTP każdego przychodzącego żądania głównie ze względów bezpieczeństwa. Domyślnie dozwolone są metody `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. + +Jeśli chcesz włączyć dodatkowe metody, takie jak `OPTIONS`, możesz użyć atrybutu `#[Requires]` (od wersji Nette Application v3.2): + +```php +#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] +class MyPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +W wersji 3.1 weryfikacja jest wykonywana w `checkHttpMethod()`, która sprawdza, czy metoda określona w żądaniu znajduje się w tablicy `$presenter->allowedMethods`. Należy dodać taką metodę: + +```php +class MyPresenter extends Nette\Application\UI\Presenter +{ + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } +} +``` + +Ważne jest, aby podkreślić, że jeśli włączysz metodę `OPTIONS`, musisz również poprawnie obsługiwać ją w swoim prezenterze. Metoda ta jest często używana jako tak zwane żądanie wstępne, które przeglądarki automatycznie wysyłają przed faktycznym żądaniem, gdy konieczne jest ustalenie, czy żądanie jest dozwolone z punktu widzenia polityki CORS (Cross-Origin Resource Sharing). Jeśli zezwolisz na tę metodę, ale nie zaimplementujesz odpowiedniej odpowiedzi, może to prowadzić do niespójności i potencjalnych problemów z bezpieczeństwem. + + Dalsza lektura .[#toc-further-reading] ====================================== diff --git a/application/pl/routing.texy b/application/pl/routing.texy index 4ce1af8644..4533a0eafb 100644 --- a/application/pl/routing.texy +++ b/application/pl/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Możemy też użyć tego formularza, zauważając nadpisanie wyrażenia regularnego walidacji: +Aby uzyskać bardziej szczegółową specyfikację, można użyć jeszcze bardziej rozszerzonej formy, w której oprócz wartości domyślnych można ustawić inne właściwości parametru, takie jak wyrażenie regularne walidacji (patrz parametr `id` ): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Te bardziej verbose formaty są przydatne do dodawania dodatkowych metadanych. +Ważne jest, aby pamiętać, że jeśli parametry zdefiniowane w tablicy nie są zawarte w masce ścieżki, ich wartości nie mogą zostać zmienione, nawet przy użyciu parametrów zapytania określonych po znaku zapytania w adresie URL. Filtry i tłumaczenia .[#toc-filters-and-translations] @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Moduły .[#toc-modules] ---------------------- -Jeśli mamy wiele tras, które wpadają do wspólnego [modułu |modules], używamy `withModule()`: +Jeśli mamy więcej tras, które należą do jednego [modułu |directory-structure#Presenters and Templates], możemy użyć `withModule()`, aby je pogrupować: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Integracja aplikacji .[#toc-integration] ======================================== -Aby zintegrować stworzony router z aplikacją, musimy powiedzieć o nim kontenerowi DI. Najprostszym sposobem na to jest przygotowanie fabryki tworzącej obiekt routera i powiedzenie konfiguracji kontenera, aby go użyć. Załóżmy, że w tym celu napiszemy metodę `App\Router\RouterFactory::createRouter()`: +Aby zintegrować stworzony router z aplikacją, musimy powiedzieć o nim kontenerowi DI. Najprostszym sposobem na to jest przygotowanie fabryki tworzącej obiekt routera i powiedzenie konfiguracji kontenera, aby go użyć. Załóżmy, że w tym celu napiszemy metodę `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ Następnie wpisujemy do [konfiguracji |dependency-injection:services]: ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Wszelkie zależności, na przykład od bazy danych itp, są przekazywane do metody fabrycznej jako jej parametry przez [autowiring |dependency-injection:autowiring]: @@ -663,7 +663,7 @@ Przez użycie samodzielne rozumiemy wykorzystanie możliwości routera w aplikac Czyli znowu tworzymy metodę, która buduje dla nas np. router: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Możemy też wykonać obiekty bezpośrednio: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/pl/templates.texy b/application/pl/templates.texy index a7dd044914..cefc14eae1 100644 --- a/application/pl/templates.texy +++ b/application/pl/templates.texy @@ -34,35 +34,81 @@ I to będzie szablon akcji: Definiuje blok `content`, który zostanie wstawiony w miejsce `{include content}` w układzie, a także redefiniuje blok `title`, który zastąpi `{block title}` w układzie. Spróbujcie sobie wyobrazić ten rezultat. -Wyszukiwanie szablonów .[#toc-search-for-templates] ---------------------------------------------------- +Wyszukiwanie szablonów .[#toc-template-lookup] +---------------------------------------------- -Ścieżka do szablonów jest wyprowadzana przez prezentera za pomocą prostej logiki. Spróbuje sprawdzić, czy jeden z tych plików znajduje się relatywnie od katalogu klasy prezentera, gdzie `` to nazwa aktualnego prezentera, a `` jest nazwą bieżącego zdarzenia: +W prezenterach nie trzeba określać, który szablon ma być renderowany; framework automatycznie określi ścieżkę, ułatwiając kodowanie. -- `templates//.latte` -- `templates/..latte` +Jeśli używasz struktury katalogów, w której każdy prezenter ma swój własny katalog, po prostu umieść szablon w tym katalogu pod nazwą akcji (tj. widoku). Na przykład dla akcji `default` należy użyć szablonu `default.latte`: -Jeśli szablon nie zostanie znaleziony, spróbuje poszukać w katalogu `templates` o jeden poziom wyżej, czyli na tym samym poziomie co katalog z klasą prezentera. +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -Jeśli tam również nie zostanie znaleziony szablon, odpowiedzią będzie [błąd 404 |presenters#Error 404 etc.]. +Jeśli używasz struktury, w której prezenterzy znajdują się w jednym katalogu, a szablony w folderze `templates`, zapisz je w pliku `..latte` lub `/.latte`: -Widok można również zmienić za pomocą strony `$this->setView('jineView')`. Lub, zamiast szukać bezpośrednio, określ nazwę pliku szablonu za pomocą `$this->template->setFile('/path/to/template.latte')`. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- + +Katalog `templates` może być również umieszczony o jeden poziom wyżej, na tym samym poziomie co katalog z klasami prezenterów. + +Jeśli szablon nie zostanie znaleziony, prezenter odpowie [błędem 404 - nie znaleziono strony |presenters#Error 404 etc]. + +Widok można zmienić za pomocą `$this->setView('anotherView')`. Możliwe jest również bezpośrednie określenie pliku szablonu za pomocą `$this->template->setFile('/path/to/template.latte')`. .[note] -Pliki, w których wyszukiwane są szablony można zmienić nakładając na metodę [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], która zwraca tablicę możliwych nazw plików. +Pliki, w których wyszukiwane są szablony, można zmienić, nadpisując metodę [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], która zwraca tablicę możliwych nazw plików. + + +Wyszukiwanie szablonu układu .[#toc-layout-template-lookup] +----------------------------------------------------------- + +Nette automatycznie wyszukuje również plik układu. + +Jeśli używasz struktury katalogów, w której każdy prezenter ma swój własny katalog, umieść układ w folderze z prezenterem, jeśli jest on specyficzny tylko dla niego, lub poziom wyżej, jeśli jest wspólny dla wielu prezenterów: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +Jeśli używasz struktury, w której prezenterzy są zgrupowani w jednym katalogu, a szablony znajdują się w folderze `templates`, layout będzie oczekiwany w następujących miejscach: -W tych plikach spodziewany jest układ: +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` układ wspólny dla wielu prezenterów +Jeśli prezenter znajduje się w module, przeszuka również dalsze części drzewa katalogów zgodnie z zagnieżdżeniem modułu. -Gdzie `` to nazwa aktualnego prezentera, a `` jest nazwą układu, którą domyślnie jest `'layout'`. Nazwę można zmienić za pomocą `$this->setLayout('jinyLayout')`, więc wypróbowane zostaną pliki `@jinyLayout.latte`. +Nazwę layoutu można zmienić za pomocą `$this->setLayout('layoutAdmin')`, a następnie będzie ona oczekiwana w pliku `@layoutAdmin.latte`. Można również bezpośrednio określić plik szablonu układu za pomocą `$this->setLayout('/path/to/template.latte')`. -Możesz również bezpośrednio określić nazwę pliku szablonu układu, używając `$this->setLayout('/path/to/template.latte')`. Użycie `$this->setLayout(false)` wyłączy śledzenie układu. +Użycie `$this->setLayout(false)` lub znacznika `{layout none}` wewnątrz szablonu wyłącza wyszukiwanie układu. .[note] -Pliki, w których wyszukiwane są szablony układów można zmienić nakładając na metodę [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], która zwraca tablicę możliwych nazw plików. +Pliki, w których przeszukiwane są szablony układu, można zmienić, nadpisując metodę [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], która zwraca tablicę możliwych nazw plików. Zmienne w szablonie .[#toc-variables-in-the-template] @@ -104,7 +150,7 @@ Adnotacja `@property-read` jest dla IDE i analizy statycznej, sprawi, że szepta Możesz również mieć luksus szeptania w szablonach, wystarczy zainstalować wtyczkę Latte w PhpStorm i umieścić nazwę klasy na początku szablonu, zobacz artykuł "Latte: jak wpisać system":https://blog.nette.org/pl/latte-jak-korzystac-z-systemu-typow, aby uzyskać więcej informacji: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void Latte w wersji 3 oferuje bardziej zaawansowany sposób tworzenia [rozszerzenia |latte:creating-extension] dla każdego projektu internetowego. Oto krótki przykład takiej klasy: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ Rejestrujemy go za pomocą [konfiguracji |configuration#Latte]: ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ Alternatywnie, tłumacz może być ustawiony za pomocą [konfiguracji |configura ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` Translator może być wtedy użyty np. jako filtr `|translate`, z dodatkowymi parametrami przekazywanymi do metody `translate()` (patrz `foo, bar`): diff --git a/application/pt/@home.texy b/application/pt/@home.texy index 68976dc90d..a9ef73c2a3 100644 --- a/application/pt/@home.texy +++ b/application/pt/@home.texy @@ -1,36 +1,85 @@ -Aplicação Nette -*************** +Aplicativo Nette +**************** .[perex] -O pacote `nette/application` é a base para a criação de aplicações web interativas. +O Nette Application é o núcleo da estrutura Nette que traz ferramentas poderosas para a criação de aplicativos modernos da Web. Ele oferece vários recursos excepcionais que simplificam significativamente o desenvolvimento e aumentam a segurança e a capacidade de manutenção do código. -- [Como funcionam as aplicações? |how-it-works] -- [Bootstrap |Bootstrap] -- [Apresentadores |Presenters] -- [Modelos |Templates] -- [Módulos |Modules] -- [Roteiro |Routing] -- [Criação de links URL |creating-links] -- [Componentes interativos |components] -- [AJAX & Snippets |ajax] -- [Multiplicador |multiplier] -- [Configuração |Configuration] +Instalação .[#toc-installation] +------------------------------- -Instalação ----------- - -Baixe e instale o pacote usando [o Composer |best-practices:composer]: +Faça o download e instale a biblioteca usando o [Composer |best-practices:composer]: ```shell composer require nette/application ``` -| versão | compatível com PHP + +Por que escolher o Aplicativo Nette? .[#toc-why-choose-nette-application] +------------------------------------------------------------------------- + +A Nette sempre foi pioneira em tecnologias da Web. + +**Roteador bidirecional:** A Nette apresenta um sistema de roteamento avançado, único em sua bidirecionalidade - ele não apenas traduz URLs para ações de aplicativos, mas também pode gerar URLs no sentido inverso. Isso significa que: +- Você pode modificar a estrutura de URL de todo o aplicativo a qualquer momento, sem modificar os arquivos de modelo +- Os URLs são automaticamente canonizados, melhorando o SEO +- O roteamento é definido em um único local, e não disperso em anotações + +**Componentes e sinais:** O sistema de componentes integrado, inspirado no Delphi e no React.js, é único entre as estruturas PHP: +- Permite a criação de elementos de UI reutilizáveis +- Oferece suporte à composição hierárquica de componentes +- Oferece tratamento elegante de solicitações AJAX usando sinais +- Rica biblioteca de componentes prontos em [Componette](https://componette.org) + +**AJAX e Snippets:** A Nette introduziu uma maneira revolucionária de trabalhar com AJAX em 2009, antes de soluções como Hotwire para Ruby on Rails ou Symfony UX Turbo: +- Os snippets permitem atualizar apenas partes da página sem escrever JavaScript +- Integração automática com o sistema de componentes +- Invalidação inteligente das seções da página +- Transferência mínima de dados + +**Modelos intuitivos [Latte |latte:]:** O sistema de modelos mais seguro para PHP com recursos avançados: +- Proteção automática contra XSS com escape sensível ao contexto +- Extensível com filtros, funções e tags personalizados +- Herança de modelos e snippets para AJAX +- Excelente suporte ao PHP 8.x com sistema de tipos + +**Injeção de dependência:** O Nette utiliza totalmente a injeção de dependência: +- Passagem automática de dependência (autowiring) +- Configuração usando o formato NEON claro +- Suporte a fábricas de componentes + + +Principais benefícios .[#toc-main-benefits] +------------------------------------------- + +- Segurança**: Proteção automática contra [vulnerabilidades |nette:vulnerability-protection] como XSS, CSRF, etc. +- **Produtividade**: Menos escrita, mais recursos graças ao design inteligente +- Depuração**: [Depurador Tracy |tracy:] com painel de roteamento +- Desempenho**: Sistema de cache inteligente, carregamento lento de componentes +- Flexibilidade**: Fácil modificação do URL mesmo após a conclusão do aplicativo +- Componentes**: Sistema exclusivo de elementos de IU reutilizáveis +- Moderno**: Suporte total para PHP 8.4+ e sistema de tipos + + +Primeiros passos .[#toc-getting-started] +---------------------------------------- + +1. Compreensão [dos aplicativos |how-it-works] - Compreensão da arquitetura básica +2. [Apresentadores |presenters] - Trabalhar com apresentadores e ações +3. [Modelos |templates] - Criação de modelos no Latte +4. [Roteamento |routing] - Configuração de URL +5. [Componentes interativos |components] - Uso do sistema de componentes + + +Compatibilidade com PHP .[#toc-php-compatibility] +------------------------------------------------- + +| Versão compatível com PHP |-----------|------------------- -| Nette Application 4.0 | PHP 8.0 - 8.2 -| Aplicação Nette 3.1 | PHP 7.2 - 8.2 -| Aplicativo Nette 3.0 | PHP 7.1 - 8.0 -| Aplicação Nette 2.4 | PHP 5.6 - 8.0 +| Aplicativo Nette 4.0 | PHP 8.1 - 8.4 +| Aplicativo Nette 3.2 | PHP 8.1 - 8.4 +| Aplicativo Nette 3.1 | PHP 7.2 - 8.3 +| Nette Application 3.0 | PHP 7.1 - 8.0 +| Aplicativo Nette 2.4 | PHP 5.6 - 8.0 -Aplica-se às últimas versões de remendos. +Válido para as versões mais recentes do patch. \ No newline at end of file diff --git a/application/pt/@left-menu.texy b/application/pt/@left-menu.texy index b44fee7d04..e81b47f895 100644 --- a/application/pt/@left-menu.texy +++ b/application/pt/@left-menu.texy @@ -4,7 +4,7 @@ Aplicação Nette - [Bootstrap |Bootstrap] - [Apresentadores |Presenters] - [Modelos |Templates] -- [Módulos |Modules] +- [Estrutura do diretório |directory-structure] - [Roteiro |Routing] - [Criação de links URL |creating-links] - [Componentes interativos |components] diff --git a/application/pt/ajax.texy b/application/pt/ajax.texy index 2ef450e4d6..c2159c60f4 100644 --- a/application/pt/ajax.texy +++ b/application/pt/ajax.texy @@ -3,40 +3,43 @@ AJAX & Snippets
    -As aplicações web modernas atualmente rodam metade em um servidor e metade em um navegador. O AJAX é um fator de união vital. Que suporte o Nette Framework oferece? -- envio de fragmentos de modelos (os chamados *snippets*) -- passando variáveis entre PHP e JavaScript -- Depuração de aplicações AJAX +Na era dos aplicativos modernos da Web, em que a funcionalidade geralmente se estende entre o servidor e o navegador, o AJAX é um elemento de conexão essencial. Que opções o Nette Framework oferece nessa área? +- envio de partes do modelo, os chamados snippets +- passagem de variáveis entre PHP e JavaScript +- ferramentas para depuração de solicitações AJAX
    -Solicitação AJAX .[#toc-ajax-request] -===================================== +Solicitação de AJAX .[#toc-ajax-request] +======================================== -Uma solicitação AJAX não difere de uma solicitação clássica - o apresentador é chamado com uma visão e parâmetros específicos. Cabe também ao apresentador como responder a ela: ele pode usar sua própria rotina, que retorna um fragmento de código HTML (HTML snippet), um documento XML, um objeto JSON ou código JavaScript. +Basicamente, uma solicitação AJAX não difere de uma solicitação HTTP clássica. Um apresentador é chamado com parâmetros específicos. Cabe ao apresentador decidir como responder à solicitação: ele pode retornar dados no formato JSON, enviar uma parte do código HTML, um documento XML etc. -No lado do servidor, uma solicitação AJAX pode ser detectada usando o método de serviço [que encapsula a solicitação HTTP |http:request] `$httpRequest->isAjax()` (detecta com base no cabeçalho HTTP `X-Requested-With`). Dentro do apresentador, um atalho está disponível na forma do método `$this->isAjax()`. +No lado do navegador, iniciamos uma solicitação AJAX usando a função `fetch()`: -Há um objeto pré-processado chamado `payload` dedicado ao envio de dados para o navegador no JSON. - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // processamento da resposta +}); ``` -Para um controle total sobre sua saída JSON, utilize o método `sendJson` em seu apresentador. Ele encerra o apresentador imediatamente e você não precisará de um modelo: +No lado do servidor, uma solicitação AJAX é reconhecida pelo método `$httpRequest->isAjax()` do serviço [que encapsula a solicitação HTTP |http:request]. Ele usa o cabeçalho HTTP `X-Requested-With`, portanto, é essencial enviá-lo. No apresentador, você pode usar o método `$this->isAjax()`. + +Se você quiser enviar dados no formato JSON, use o método [`sendJson()` |presenters#Sending a response] método. O método também encerra a atividade do apresentador. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Se quisermos enviar HTML, podemos definir um modelo especial para pedidos AJAX: +Se você planeja responder com um modelo especial projetado para AJAX, pode fazer isso da seguinte forma: ```php public function handleClick($param): void @@ -44,27 +47,43 @@ public function handleClick($param): void if ($this->isAjax()) { $this->template->setFile('path/to/ajax.latte'); } - // ... + //... } ``` +Trechos .[#toc-snippets] +======================== + +A ferramenta mais poderosa oferecida pela Nette para conectar o servidor com o cliente são os snippets. Com eles, você pode transformar um aplicativo comum em um aplicativo AJAX com o mínimo de esforço e algumas linhas de código. O exemplo Fifteen demonstra como tudo isso funciona, e seu código pode ser encontrado no [GitHub |https://github.com/nette-examples/fifteen]. + +Os snippets, ou recortes, permitem que você atualize apenas partes da página, em vez de recarregar a página inteira. Isso é mais rápido e mais eficiente, além de proporcionar 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. É interessante notar que a Nette introduziu os snippets 14 anos antes. + +Como os snippets funcionam? Quando a página é carregada pela primeira vez (uma solicitação não AJAX), a página inteira, incluindo todos os snippets, é carregada. 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, é feita uma solicitação AJAX. O código no apresentador executa a ação e decide quais trechos precisam ser atualizados. A Nette renderiza esses trechos e os envia na forma de uma matriz JSON. O código de manipulação no navegador insere os trechos recebidos de volta na página. Portanto, somente o código dos trechos alterados é transferido, economizando largura de banda e acelerando o carregamento em comparação com a transferência de todo o conteúdo da página. + + Naja .[#toc-naja] -================= +----------------- -A [biblioteca Naja |https://naja.js.org] é utilizada para lidar com pedidos AJAX no lado do navegador. [Instale-a |https://naja.js.org/#/guide/01-install-setup-naja] como um pacote node.js (para usar com Webpack, Rollup, Vite, Parcel e mais): +Para lidar com snippets no navegador, é usada 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 aplicativos como Webpack, Rollup, Vite, Parcel e outros): ```shell npm install naja ``` -...ou inseri-lo diretamente no modelo da página: +... ou insira-a diretamente no modelo da página: ```html ``` -Para criar uma solicitação AJAX a partir de um link regular (sinal) ou envio de formulário, basta marcar o link, formulário ou botão relevante com a classe `ajax`: +Primeiro, você precisa [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 solicitação AJAX, basta marcar o respectivo link, formulário ou botão com a classe `ajax`: ```html Go @@ -74,64 +93,39 @@ Para criar uma solicitação AJAX a partir de um link regular (sinal) ou envio d or +
    ``` -Snippets .[#toc-snippets] -========================= - -Há uma ferramenta muito mais poderosa de suporte AJAX incorporado - trechos. O uso deles torna possível transformar uma aplicação regular em AJAX, utilizando apenas algumas linhas de código. Como tudo funciona é demonstrado no exemplo dos Quinze, cujo código também é acessível no build ou no [GitHub |https://github.com/nette-examples/fifteen]. - -A forma como os trechos funcionam é que a página inteira é transferida durante a solicitação inicial (isto é, não-AJAX) e depois com cada [sub solicitação |components#signal] AJAX (solicitação da mesma visão do mesmo apresentador) apenas o código das partes alteradas é transferido no repositório `payload` mencionado anteriormente. - -Snippets podem lembrá-lo da Hotwire para Ruby on Rails ou Symfony UX Turbo, mas a Nette surgiu com eles catorze anos antes. - +Redesenho de snippets .[#toc-redrawing-snippets] +------------------------------------------------ -Invalidação de Snippets .[#toc-invalidation-of-snippets] -======================================================== - -Cada descendente do [Controle de |components] Classe (que um Apresentador também é) é capaz de lembrar se houve alguma mudança durante um pedido que requeira sua reapresentação. Há um par de métodos para lidar com isso: `redrawControl()` e `isControlInvalid()`. Um exemplo: +Todos os objetos da classe [Control |components] (inclusive o próprio Presenter) mantêm um registro da ocorrência de alterações que exijam o redesenho. O método `redrawControl()` é usado para essa finalidade. ```php public function handleLogin(string $user): void { - // O objeto tem de ser restituído após o usuário ter feito o login + // depois de fazer login, é necessário redesenhar a parte relevante $this->redrawControl(); - // ... + //... } ``` -A Nette, entretanto, oferece uma resolução ainda mais fina do que os componentes inteiros. Os métodos listados aceitam o nome do chamado "snippet" como um parâmetro opcional. Um "snippet" é basicamente um elemento em seu modelo marcado para esse fim por uma tag Latte, mais sobre isso depois. Assim, é possível pedir a um componente para redesenhar apenas *partes* de seu gabarito. Se o componente inteiro for invalidado, então todos os seus trechos serão restituídos. Um componente é "inválido" também se qualquer um de seus subcomponentes for inválido. - -```php -$this->isControlInvalid(); // -> false -$this->redrawControl('header'); // invalida o snippet chamado 'header'. -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, at least one snippet is invalid +O Nette também permite um controle mais preciso do que precisa ser redesenhado. O método mencionado acima pode usar o nome do snippet como argumento. Assim, é possível invalidar (ou seja, forçar um redesenho) no nível da parte do modelo. Se o componente inteiro for invalidado, todos os trechos dele também serão redesenhados: -$this->redrawControl(); // invalida todo o componente, todos os snippet -$this->isControlInvalid('footer'); // -> true +```php +// invalida o trecho de "cabeçalho +$this->redrawControl('header'); ``` -Um componente que recebe um sinal é automaticamente marcado para ser redesenhado. - -Graças ao desenho de snippet, sabemos exatamente quais partes de quais elementos devem ser novamente entregues. - - -Tag `{snippet} … {/snippet}` .{toc: Tag snippet} -================================================ - -A renderização da página procede de forma muito semelhante a um pedido regular: os mesmos modelos são carregados, etc. A parte vital é, no entanto, deixar de fora as partes que não devem chegar à saída; as outras partes devem ser associadas a um identificador e enviadas ao usuário em um formato compreensível para um manipulador de JavaScript. - -Sintaxe .[#toc-syntax] ----------------------- +Snippets em Latte .[#toc-snippets-in-latte] +------------------------------------------- -Se houver um controle ou um snippet no modelo, temos que embrulhá-lo usando a tag do par `{snippet} ... {/snippet}` - ele assegurará que o snippet renderizado será "cortado" e enviado para o navegador. Ele também o anexará em um helper `
    ` (é possível utilizar uma etiqueta diferente). No exemplo a seguir, um trecho chamado `header` está definido. Ele pode também representar o modelo de um componente: +Usar snippets no Latte é extremamente fácil. Para definir uma parte do modelo como um snippet, basta envolvê-la nas tags `{snippet}` e `{/snippet}`: ```latte {snippet header} @@ -139,7 +133,9 @@ Se houver um controle ou um snippet no modelo, temos que embrulhá-lo usando a t {/snippet} ``` -Um fragmento de um tipo diferente de `
    ` ou um snippet com atributos HTML adicionais é obtido usando a variante de atributo: +O snippet cria um elemento `
    ` na página HTML com um `id` especialmente gerado. Ao redesenhar um snippet, o conteúdo desse elemento é atualizado. Portanto, quando a página é renderizada inicialmente, todos os snippets também devem ser renderizados, mesmo que inicialmente estejam vazios. + +Você também pode criar um snippet com um elemento diferente de `
    ` usando um atributo n:: ```latte
    @@ -148,138 +144,106 @@ Um fragmento de um tipo diferente de `
    ` ou um snippet com atributos HTML ad ``` -Snippets dinâmicos .[#toc-dynamic-snippets] -=========================================== +Áreas de snippet .[#toc-snippet-areas] +-------------------------------------- -Em Nette você também pode definir trechos com um nome dinâmico baseado em um parâmetro de tempo de execução. Isto é mais adequado para várias listas onde precisamos mudar apenas uma linha, mas não queremos transferir a lista inteira junto com ela. Um exemplo disto seria: +Os nomes dos snippets também podem ser expressões: ```latte -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Há um trecho estático chamado `itemsContainer`, contendo vários trechos dinâmicos: `item-0`, `item-1` e assim por diante. +Dessa forma, obteremos vários snippets como `item-0`, `item-1`, etc. Se invalidássemos diretamente um snippet dinâmico (por exemplo, `item-1`), nada seria redesenhado. O motivo é que os snippets funcionam como verdadeiros trechos e somente eles são renderizados diretamente. Entretanto, no modelo, não há tecnicamente um trecho chamado `item-1`. Ele só aparece quando se executa o código ao redor do snippet, nesse caso, o loop foreach. Por isso, marcaremos a parte do modelo que precisa ser executada com a tag `{snippetArea}`: -Você não pode redesenhar um trecho dinâmico diretamente (o redesenho de `item-1` não tem efeito), você tem que redesenhar seu trecho pai (neste exemplo `itemsContainer`). Isto faz com que o código do snippet pai seja executado, mas então apenas seus sub-snippets são enviados para o navegador. Se você quiser enviar apenas um dos sub-snippets, você tem que modificar a entrada para que o trecho pai não gere os outros sub-snippets. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -No exemplo acima você tem que ter certeza de que para um pedido AJAX apenas um item será adicionado à matriz `$list`, portanto o laço `foreach` imprimirá apenas um trecho dinâmico. +E redesenharemos tanto o snippet individual quanto toda a área abrangente: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * This method returns data for the list. - * Usually this would just request the data from a model. - * For the purpose of this example, the data is hard-coded. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +Também é essencial garantir que a matriz `$items` contenha apenas os itens que devem ser redesenhados. -Snippets em um Modelo Incluído .[#toc-snippets-in-an-included-template] -======================================================================= - -Pode acontecer que o trecho esteja em um modelo que está sendo incluído a partir de um modelo diferente. Nesse caso, precisamos embrulhar o código de inclusão no segundo modelo com a tag `snippetArea`, então redesenhamos tanto o snippetArea quanto o snippet real. - -A tag `snippetArea` assegura que o código interno seja executado, mas apenas o trecho real no modelo incluído é enviado para o navegador. +Ao inserir outro modelo no modelo principal usando a tag `{include}`, que tem snippets, é necessário envolver novamente o modelo incluído em um `snippetArea` e invalidar o snippet e a área juntos: ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* incluído.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -Você também pode combiná-lo com trechos dinâmicos. +Snippets em componentes .[#toc-snippets-in-components] +------------------------------------------------------ -Adicionando e excluindo .[#toc-adding-and-deleting] -=================================================== - -Se você acrescentar um novo item à lista e invalidar `itemsContainer`, o pedido AJAX devolve trechos incluindo o novo, mas o manipulador de javascript não será capaz de renderizá-lo. Isto porque não há nenhum elemento HTML com o ID recém-criado. - -Neste caso, a maneira mais simples é envolver toda a lista em mais um trecho e invalidar tudo isso: +Você pode criar snippets em [componentes |components], e o Nette os redesenha automaticamente. Entretanto, há uma limitação específica: para redesenhar os snippets, ele chama o método `render()` sem nenhum parâmetro. Portanto, a passagem de parâmetros no modelo não funcionará: ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Envio de dados do usuário .[#toc-sending-user-data] +--------------------------------------------------- + +Juntamente com os snippets, você pode enviar quaisquer dados adicionais ao cliente. Basta gravá-los no objeto `payload`: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + //... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -O mesmo vale para a eliminação de um item. Seria possível enviar um trecho vazio, mas geralmente as listas podem ser paginadas e seria complicado implementar a exclusão de um item e o carregamento de outro (que costumava estar em uma página diferente da lista paginada). - -Parâmetros de envio para o componente .[#toc-sending-parameters-to-component] -============================================================================= +Parâmetros de envio .[#toc-sending-parameters] +============================================== -Quando enviamos parâmetros para o componente via solicitação AJAX, sejam parâmetros de sinal ou parâmetros persistentes, devemos fornecer seu nome global, que também contém o nome do componente. O nome completo do parâmetro retorna o método `getParameterId()`. +Quando enviamos parâmetros ao componente por meio de solicitação AJAX, sejam eles parâmetros de sinal ou parâmetros persistentes, devemos fornecer seu nome global, que também contém o nome do componente. O nome completo do parâmetro retorna o método `getParameterId()`. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -E método de manuseio com s parâmetros correspondentes em componente. +Um método handle com os parâmetros correspondentes no componente: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/pt/bootstrap.texy b/application/pt/bootstrap.texy index 1ebef20eff..61a9aa9014 100644 --- a/application/pt/bootstrap.texy +++ b/application/pt/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + private Configurator $configurator; + private string $rootDir; + + public function __construct() + { + $this->rootDir = dirname(__DIR__); + // O configurador é responsável pela configuração do ambiente e dos serviços do aplicativo. + $this->configurator = new Configurator; + // Defina o diretório para os arquivos temporários gerados pelo Nette (por exemplo, modelos 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 { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // O Nette é inteligente e o modo de desenvolvimento é ativado automaticamente, + // ou você pode ativá-lo para um endereço IP específico, descomentando a seguinte linha: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Habilita o Tracy: a melhor ferramenta de depuração do tipo "canivete suíço". + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: carrega automaticamente todas as classes no diretório fornecido + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Carregar arquivos de configuração + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php .[#toc-index-php] =========================== -No caso de aplicações web, o arquivo inicial é `index.php`, que está localizado no diretório público `www/`. Ele permite que a classe `Bootstrap` inicialize o ambiente e devolva o `$configurator` que cria o contêiner DI. Em seguida, obtém o serviço `Application`, que executa a aplicação web: +No caso de aplicativos Web, o arquivo principal é `index.php`, que está localizado no [diretório público |directory-structure#public-directory-www] `www/`. Isso fará com que a classe Bootstrap inicialize o ambiente e produza um contêiner DI. Em seguida, ela obtém o serviço `Application` a partir dele, que inicia o aplicativo Web: ```php -// inicializar o ambiente + obter objeto Configurador -$configurator = App\Bootstrap::boot(); -// criar um recipiente DI -$container = $configurator->createContainer(); -// Recipiente DI cria um objeto de aplicação Nette +$bootstrap = new App\Bootstrap; +// Inicializar o ambiente + criar um contêiner DI +$container = $bootstrap->bootWebApplication(); +// O contêiner DI cria um objeto Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -// iniciar a aplicação Nette +// Iniciar o aplicativo Nette e tratar a solicitação de entrada $application->run(); ``` @@ -59,26 +84,42 @@ Como você pode ver, a classe [api:Nette\Bootstrap\Configurator], que agora vamo Desenvolvimento vs Modo de Produção .[#toc-development-vs-production-mode] ========================================================================== -Nette distingue dois modos básicos nos quais uma solicitação é executada: desenvolvimento e produção. O modo de desenvolvimento é focado no máximo conforto do programador, Tracy é exibido, o cache é atualizado automaticamente ao alterar os modelos ou a configuração do container DI, etc. O modo de produção é focado no desempenho, Tracy apenas registra erros e mudanças de gabaritos e outros arquivos não são verificados. +O Nette se comporta de forma diferente, dependendo se está sendo executado em um servidor de desenvolvimento ou de produção: + +🛠️ Development Mode (Modo de desenvolvimento): + - Exibe a barra de depuração Tracy com informações úteis (por exemplo, consultas SQL, tempo de execução, uso de memória). + - Mostra uma página de erro detalhada com rastros de chamadas de função e conteúdo de variáveis quando ocorre um erro. + - Atualiza automaticamente o cache quando modelos Latte, arquivos de configuração etc. são modificados. + + +Modo de produção: + - Não exibe nenhuma informação de depuração; todos os erros são registrados. + - Mostra um `ErrorPresenter` ou uma página genérica de "Erro do servidor" quando ocorre um erro. + - O cache nunca é atualizado automaticamente! + - Otimizado para velocidade e segurança. -A seleção do modo é feita por autodetecção, de modo que normalmente não há necessidade de configurar ou trocar nada manualmente. O modo é desenvolvimento se a aplicação estiver rodando no localhost (ou seja, endereço IP `127.0.0.1` ou `::1`) e nenhum proxy estiver presente (ou seja, seu cabeçalho HTTP). Caso contrário, ele é executado em modo de produção. + +O modo é determinado automaticamente, portanto, na maioria dos casos, não há necessidade de configurá-lo ou alterná-lo manualmente: + +- Modo de desenvolvimento: Ativo no localhost (endereço IP `127.0.0.1` ou `::1`), a menos que um proxy esteja em uso (ou seja, com base em seus cabeçalhos HTTP). +- Modo de produção: Ativo em todos os outros lugares. Se você quiser ativar o modo de desenvolvimento em outros casos, por exemplo, para programadores que acessam de um endereço IP específico, você pode usar `setDebugMode()`: ```php -$configurator->setDebugMode('23.75.345.200'); // um ou mais endereços IP +$this->configurator->setDebugMode('23.75.345.200'); // um ou mais endereços IP ``` Definitivamente, recomendamos combinar um endereço IP com um cookie. Armazenaremos um token secreto no cookie `nette-debug`, por exemplo `secret1234`, e o modo de desenvolvimento será ativado para programadores com esta combinação de IP e cookie. ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` Também podemos desligar completamente o modo desenvolvedor, mesmo para o localhost: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` Note que o valor `true` liga o modo desenvolvedor por hard, o que nunca deveria acontecer em um servidor de produção. @@ -90,7 +131,7 @@ Ferramenta de depuração Tracy .[#toc-debugging-tool-tracy] Para facilitar a depuração, vamos acionar a grande ferramenta [Tracy |tracy:]. No modo desenvolvedor ela visualiza os erros e no modo de produção registra os erros no diretório especificado: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ Arquivos temporários .[#toc-temporary-files] Nette utiliza o cache para contêiner DI, RobotLoader, modelos, etc. Portanto, é necessário definir o caminho para o diretório onde o cache será armazenado: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` No Linux ou macOS, defina as [permissões de escrita |nette:troubleshooting#Setting directory permissions] para os diretórios `log/` e `temp/`. @@ -112,7 +153,7 @@ RobotLoader .[#toc-robotloader] Normalmente, queremos carregar automaticamente as classes usando [o RobotLoader |robot-loader:], então temos que iniciá-lo e deixá-lo carregar classes do diretório onde se encontra `Bootstrap.php` (ou seja, `__DIR__`) e todos os seus subdiretórios: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ Fuso horário .[#toc-timezone] O Configurador permite que você especifique um fuso horário para sua aplicação. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ No modo de desenvolvimento, o recipiente é atualizado automaticamente cada vez Os arquivos de configuração são carregados usando `addConfig()`: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` O método `addConfig()` pode ser chamado várias vezes para adicionar vários arquivos. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ Parâmetros estáticos .[#toc-static-parameters] Os parâmetros usados nos arquivos de configuração podem ser definidos [na seção `parameters` |dependency-injection:configuration#parameters] e também passados (ou sobrescritos) pelo método `addStaticParameters()` (tem o pseudônimo `addParameters()`). É importante que diferentes valores de parâmetros causem a geração de recipientes DI adicionais, ou seja, classes adicionais. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ Parâmetros dinâmicos .[#toc-dynamic-parameters] Também podemos adicionar parâmetros dinâmicos ao recipiente, seus diferentes valores, ao contrário dos parâmetros estáticos, não causarão a geração de novos recipientes DI. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ As variáveis ambientais poderiam ser facilmente disponibilizadas usando parâmetros dinâmicos. Podemos acessá-las via `%env.variable%` em arquivos de configuração. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ Você pode usar os seguintes parâmetros estáticos nos arquivos de configuraç - `%wwwDir%` é o caminho absoluto para o diretório que contém 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 o pedido veio através da linha de comando @@ -225,7 +268,7 @@ services: Criar uma nova instância e inseri-la no bootstrap: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Diferentes Ambientes .[#toc-different-environments] =================================================== -Sinta-se à vontade para personalizar a classe `Bootstrap` de acordo com suas necessidades. Você pode adicionar parâmetros ao método `boot()` para diferenciar projetos web, ou adicionar outros métodos, tais como `bootForTests()`, que inicializa o ambiente para testes unitários, `bootForCli()` para scripts chamados a partir da linha de comando, e assim por diante. +Não hesite em personalizar a classe `Bootstrap` de acordo com suas necessidades. Você pode adicionar parâmetros ao método `bootWebApplication()` para diferenciar os projetos da Web. Como alternativa, você pode adicionar outros métodos, como `bootTestEnvironment()` para inicializar o ambiente para testes de unidade, `bootConsoleApplication()` para scripts chamados pela linha de comando e assim por diante. ```php -public static function bootForTests(): Configurator +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 { - $configurator = self::boot(); - Tester\Environment::setup(); // Nette Tester initialization - return $configurator; + $this->configurator->setDebugMode(false); + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); } ``` diff --git a/application/pt/components.texy b/application/pt/components.texy index e8df613355..502dc15cdc 100644 --- a/application/pt/components.texy +++ b/application/pt/components.texy @@ -230,6 +230,28 @@ No modelo, estas mensagens estão disponíveis na variável `$flashes` como obje ``` +Redirecionamento após um sinal .[#toc-redirection-after-a-signal] +================================================================= + +Depois de processar um sinal de componente, o redirecionamento geralmente é feito. Essa situação é semelhante à dos formulários: após o envio de um formulário, também redirecionamos para evitar o reenvio de dados quando a página é atualizada no navegador. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Como um componente é um elemento reutilizável e normalmente não deve ter uma dependência direta de apresentadores específicos, os métodos `redirect()` e `link()` interpretam automaticamente o parâmetro como um sinal de componente: + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +Se precisar redirecionar para um apresentador ou ação diferente, você poderá fazer isso por meio do apresentador: + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Parâmetros Persistentes .[#toc-persistent-parameters] ===================================================== @@ -347,7 +369,7 @@ services: Finalmente, utilizaremos esta fábrica em nosso apresentador: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ Componentes em profundidade .[#toc-components-in-depth] Os componentes de uma aplicação Nette são as partes reutilizáveis de uma aplicação web que incorporamos nas páginas, que é o tema deste capítulo. Quais são exatamente as capacidades de um componente desse tipo? 1) é renderizável em um modelo -2) ela sabe qual parte de si mesma deve prestar durante um [pedido AJAX |ajax#invalidation] (trechos) +2) ele sabe [qual parte de si mesmo |ajax#snippets] deve ser renderizada durante uma solicitação AJAX (snippets) 3) tem a capacidade de armazenar seu estado em uma URL (parâmetros persistentes) 4) tem a capacidade de responder às ações (sinais) do usuário 5) cria uma estrutura hierárquica (onde a raiz é o apresentador) @@ -430,7 +452,7 @@ class PaginatingControl extends Control } ``` -O processo oposto, ou seja, a coleta de valores de properites persistentes, é tratado pelo método `saveState()`. +O processo oposto, ou seja, a coleta de valores de properties persistentes, é tratado pelo método `saveState()`. Sinais em profundidade .[#toc-signals-in-depth] diff --git a/application/pt/configuration.texy b/application/pt/configuration.texy index b4561d709c..38e367ec51 100644 --- a/application/pt/configuration.texy +++ b/application/pt/configuration.texy @@ -13,11 +13,15 @@ application: # mostra painel "Aplicação Nette" em Tracy BlueScreen? debugger: ... # (bool) por omissão - # será que o apresentador de erro será chamado por erro? - catchExceptions: ... # (bool) é o padrão verdadeiro no modo de produção + # O apresentador de erros será chamado no erro? + # tem efeito somente no modo de desenvolvedor + catchExceptions: ... # (bool) tem como padrão true # nome do apresentador de erro - errorPresenter: Error # (string) padrão para 'Nette:Erro'. + errorPresenter: Error # (string|array) padrão para 'Nette:Erro'. + + # define aliases para apresentadores e eventos + aliases: ... # define as regras para resolver o nome do apresentador para uma classe mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) falha em falso ``` -Como os apresentadores de erros não são chamados por padrão no modo de desenvolvimento e os erros são exibidos por Tracy, alterar o valor `catchExceptions` para `true` ajuda a verificar se os apresentadores de erros funcionam corretamente durante o desenvolvimento. +A partir da versão 3.2 do site `nette/application`, é possível definir um par de apresentadores de erros: + +```neon +application: + errorPresenter: + 4xx: Error4xx # para Nette\Application\BadRequestException + 5xx: Error5xx # para outras exceções +``` A opção `silentLinks` determina como Nette se comporta em modo desenvolvedor quando a geração de links falha (por exemplo, porque não há apresentador, etc.). O valor padrão `false` significa que a Nette aciona `E_USER_WARNING`. A configuração para `true` suprime esta mensagem de erro. Em um ambiente de produção, `E_USER_WARNING` é sempre invocado. Também podemos influenciar este comportamento definindo a variável apresentadora [$invalidLinkMode |creating-links#Invalid Links]. -O [mapeamento define as regras |modules#mapping] pelas quais o nome da classe é derivado do nome do apresentador. +[Os apelidos simplificam a referência a |creating-links#aliases] apresentadores usados com frequência. + +O [mapeamento define as regras |directory-structure#Presenter Mapping] pelas quais o nome da classe é derivado do nome do apresentador. Registro automático dos apresentadores .[#toc-automatic-registration-of-presenters] @@ -82,6 +95,9 @@ latte: # Permite [verificar o código gerado |latte:develop#Checking Generated Code] phpLinter: ... # (string) o padrão é null + # Define a localidade + locale: cs_CZ # (string) o padrão é nulo + # classe de $this->template templateClass: App\MyTemplateClass # defaults to Nette\Bridges\ApplicationLatte\DefaultTemplate ``` @@ -91,7 +107,7 @@ Se você estiver usando Latte versão 3, você pode adicionar uma nova [extensã ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` /--comment diff --git a/application/pt/creating-links.texy b/application/pt/creating-links.texy index b58d739ab1..554ad0d30a 100644 --- a/application/pt/creating-links.texy +++ b/application/pt/creating-links.texy @@ -38,7 +38,7 @@ Também é possível passar parâmetros nomeados. O seguinte link passa o parâm detail ``` -Se o método `ProductPresenter::renderShow()` não tiver `$lang` em sua assinatura, ele pode ler o valor do parâmetro usando `$lang = $this->getParameter('lang')`. +Se o método `ProductPresenter::renderShow()` não tiver `$lang` em sua assinatura, ele poderá recuperar o valor do parâmetro usando `$lang = $this->getParameter('lang')` ou a partir da [propriedade |presenters#Request Parameters]. Se os parâmetros estiverem armazenados em uma matriz, eles podem ser expandidos com o operador `...` (ou `(expand)` operador em Latte 2.x): @@ -103,7 +103,7 @@ Se a ação é `default`, podemos omiti-la, mas o cólon deve permanecer: home ``` -Os links também podem apontar para outros [módulos |modules]. Aqui, os links são diferenciados em relativos aos submódulos, ou absolutos. O princípio é análogo aos caminhos do disco, somente em vez de cortes existem colons. Vamos supor que o apresentador real faça parte do módulo `Front`, então escreveremos: +Os links também podem apontar para outros [módulos |directory-structure#Presenters and Templates]. Aqui, os links são diferenciados em relativos aos submódulos ou absolutos. O princípio é análogo ao dos caminhos de disco, só que, em vez de barras, há dois pontos. Vamos supor que o apresentador real faça parte do módulo `Front`, então escreveremos: ```latte link to Front:Shop:Product:show @@ -140,7 +140,7 @@ A meta `this` irá criar um link para a página atual: refresh ``` -Ao mesmo tempo, todos os parâmetros especificados na assinatura do `render()` ou `action()` método são transferidos. Portanto, se estivermos nas páginas `Product:show` e `id:123`, o link para `this` também passará este parâmetro. +Ao mesmo tempo, todos os parâmetros especificados na assinatura do parâmetro `action()` ou `render()` se o método `action()` não estiver definido, serão transferidos. Portanto, se estivermos nas páginas `Product:show` e `id:123`, o link para `this` também passará esse parâmetro. Naturalmente, é possível especificar os parâmetros diretamente: @@ -213,7 +213,7 @@ Como [os componentes |components] são unidades reutilizáveis separadas que nã Se quisermos fazer um link para apresentadores no modelo de componente, usamos a tag `{plink}`: ```latte -home +home ``` ou no código @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Pseudônimos .[#toc-aliases]{data-version:v3.2.2} +================================================ + +Às vezes, é útil atribuir um alias facilmente memorável a um par Presenter:action. Por exemplo, você poderia nomear a página inicial `Front:Home:default` simplesmente como `home` ou `Admin:Dashboard:default` como `admin`. + +Os 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 escritos usando o símbolo at, por exemplo: + +```latte +administration +``` + +Eles são compatíveis com todos os métodos que funcionam com links, como `redirect()` e similares. + + Links inválidos .[#toc-invalid-links] ===================================== @@ -257,6 +281,6 @@ Como criar vínculos com o método `link()` conforto, mas sem a presença de um O LinkGenerator é um serviço que você pode ter passado pelo construtor e depois criar links usando seu método `link()`. -Há uma diferença em comparação com os apresentadores. O LinkGenerator cria todos os links como URLs absolutos. Além disso, não há "apresentador atual", portanto não é possível especificar apenas o nome da ação `link('default')` ou os caminhos relativos aos [módulos |modules]. +Em comparação com os apresentadores, há uma diferença. O LinkGenerator cria todos os links diretamente como URLs absolutos. Além disso, não há um "apresentador real", portanto, você não pode simplesmente listar o nome da ação `link('default')` como o destino ou listar caminhos relativos aos 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 new file mode 100644 index 0000000000..e285488eac --- /dev/null +++ b/application/pt/directory-structure.texy @@ -0,0 +1,526 @@ +Estrutura de diretórios do aplicativo +************************************* + +
    + +Como criar uma estrutura de diretórios clara e dimensionável para projetos no Nette Framework? Mostraremos práticas comprovadas que o ajudarão a organizar seu código. Você aprenderá: + +- como **estruturar logicamente** o aplicativo em diretórios +- como projetar a estrutura para **escalar bem** à medida que o projeto cresce +- quais são as **possíveis alternativas** e suas vantagens ou desvantagens + +
    + + +É importante mencionar que o Nette Framework em si não insiste em nenhuma estrutura específica. Ele foi projetado para ser facilmente adaptável a quaisquer necessidades e preferências. + + +Estrutura básica do projeto .[#toc-basic-project-structure] +=========================================================== + +Embora o Nette Framework não determine nenhuma estrutura de diretório fixa, há um arranjo padrão comprovado na forma de [Projeto Web |https://github.com/nette/web-project]: + +/--pre +web-project/ +├── app/ ← diretório do aplicativo +├── assets/ ← SCSS, arquivos JS, imagens..., alternativamente resources/ +├── bin/ ← scripts de 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 (raiz do documento) +\-- + +Você pode modificar livremente essa estrutura de acordo com suas necessidades - renomear ou mover pastas. Em seguida, basta ajustar os caminhos relativos aos diretórios em `Bootstrap.php` e, possivelmente, em `composer.json`. Nada mais é necessário, nenhuma reconfiguração complexa, nenhuma alteração constante. O Nette tem detecção automática inteligente e reconhece automaticamente o local do aplicativo, incluindo sua base de URL. + + +Princípios de organização do código .[#toc-code-organization-principles] +======================================================================== + +Quando você explora um novo projeto pela primeira vez, deve ser capaz de se orientar rapidamente. Imagine clicar no diretório `app/Model/` e ver esta estrutura: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +Com isso, você só saberá que o projeto usa alguns serviços, repositórios e entidades. Você não saberá nada sobre a finalidade real do aplicativo. + +Vamos dar uma olhada em uma abordagem diferente: **organização por domínios**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Isso é diferente - à primeira vista, fica claro que se trata de um site de comércio eletrônico. Os próprios nomes dos diretórios revelam o que o aplicativo pode fazer: ele trabalha com pagamentos, pedidos e produtos. + +A primeira abordagem (organização por tipo de classe) traz vários problemas na prática: o código que é logicamente relacionado está espalhado em pastas diferentes e você precisa pular entre elas. Portanto, organizaremos por domínios. + + +Espaços de nome .[#toc-namespaces] +---------------------------------- + +É convencional que a estrutura de diretórios corresponda aos namespaces no aplicativo. Isso significa que o local físico dos arquivos corresponde ao namespace deles. Por exemplo, uma classe localizada em `app/Model/Product/ProductRepository.php` deve ter o namespace `App\Model\Product`. Esse princípio ajuda na orientação do código e simplifica o carregamento automático. + + +Singular vs. Plural em nomes .[#toc-singular-vs-plural-in-names] +---------------------------------------------------------------- + +Observe que usamos o singular para os principais diretórios de aplicativos: `app`, `config`, `log`, `temp`, `www`. O mesmo se aplica dentro do aplicativo: `Model`, `Core`, `Presentation`. Isso ocorre porque cada um representa um conceito unificado. + +Da mesma forma, `app/Model/Product` representa tudo sobre produtos. Não o chamamos de `Products` porque não se trata de uma pasta cheia de produtos (que conteria arquivos como `iphone.php`, `samsung.php`). É um namespace que contém 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 autônomos - `CleanupTask.php`, `ImportTask.php`. Cada um deles é uma unidade independente. + +Para fins de consistência, recomendamos o uso de: +- Singular para namespaces que representam uma unidade funcional (mesmo se estiver trabalhando com várias entidades) +- Plural para coleções de unidades independentes +- Em caso de incerteza ou se você não quiser pensar sobre isso, escolha singular + + +Diretório público `www/` .[#toc-public-directory-www] +===================================================== + +Esse diretório é o único acessível pela Web (o chamado document-root). É possível que você encontre com frequência o nome `public/` em vez de `www/` - é apenas uma questão de convenção e não afeta a funcionalidade. O diretório contém: +- [Ponto de entrada |bootstrap#index.php] do aplicativo `index.php` +- Arquivo `.htaccess` com regras mod_rewrite (para Apache) +- Arquivos estáticos (CSS, JavaScript, imagens) +- Arquivos carregados + +Para garantir a segurança adequada do aplicativo, é fundamental ter [o document-root configurado |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url] corretamente. + +.[note] +Nunca coloque a pasta `node_modules/` nesse diretório, pois ela contém milhares de arquivos que podem ser executáveis e não devem ser acessíveis ao público. + + +Diretório de aplicativos `app/` .[#toc-application-directory-app] +================================================================= + +Este é o diretório principal com o código do aplicativo. Estrutura básica: + +/--pre +app/ +├── Core/ ← questões de infraestrutura +├── Model/ ← lógica de negócios +├── Presentation/ ← apresentadores e modelos +├── Tasks/ ← scripts de comando +└── Bootstrap.php ← classe bootstrap do aplicativo +\-- + +`Bootstrap.php` é a [classe de inicialização do aplicativo |bootstrap] que inicializa o ambiente, carrega a configuração e cria o contêiner DI. + +Vamos agora examinar detalhadamente os subdiretórios individuais. + + +Apresentadores e modelos .[#toc-presenters-and-templates] +========================================================= + +Temos a parte de apresentação do aplicativo no diretório `app/Presentation`. Uma alternativa é o curto `app/UI`. Esse é o local para todos os apresentadores, seus modelos e todas as classes auxiliares. + +Organizamos essa camada por domínios. Em um projeto complexo que combina comércio eletrônico, blog e API, a estrutura seria semelhante a esta: + +/--pre +app/Presentation/ +├── Shop/ ← front-end de comércio eletrônico +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← blog +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administração +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← Pontos de extremidade da API + └── V1/ +\-- + +Por outro lado, para um blog simples, usaríamos esta estrutura: + +/--pre +app/Presentation/ +├── Front/ ← front-end do site +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administração +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, sitemaps etc. +\-- + +Pastas como `Home/` ou `Dashboard/` contêm apresentadores e modelos. Pastas como `Front/`, `Admin/` ou `Api/` são chamadas de **módulos**. Tecnicamente, esses são diretórios regulares que servem para a organização lógica do aplicativo. + +Cada pasta com um apresentador contém um apresentador com o mesmo nome e seus modelos. Por exemplo, a pasta `Dashboard/` contém: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← apresentador +└── default.latte ← modelo +\-- + +Essa estrutura de diretório é refletida nos namespaces de classe. Por exemplo, `DashboardPresenter` está no namespace `App\Presentation\Admin\Dashboard` (consulte o [mapeamento do apresentador |#presenter mapping]): + +```php +namespace App\Presentation\Admin\Dashboard; + +class DashboardPresenter extends Nette\Application\UI\Presenter +{ + //... +} +``` + +Referimo-nos ao apresentador `Dashboard` dentro do módulo `Admin` no aplicativo usando a notação de dois pontos como `Admin:Dashboard`. Para sua ação `default`, então, como `Admin:Dashboard:default`. Para módulos aninhados, usamos mais dois-pontos, por exemplo, `Shop:Order:Detail:default`. + + +Desenvolvimento de estrutura flexível .[#toc-flexible-structure-development] +---------------------------------------------------------------------------- + +Uma das grandes vantagens dessa estrutura é a elegância com que ela se adapta às necessidades crescentes do projeto. Como exemplo, vamos pegar a parte que gera feeds XML. Inicialmente, temos um formulário simples: + +/--pre +Export/ +├── ExportPresenter.php ← um apresentador para todas as exportações +├── sitemap.latte ← modelo para mapa do site +└── feed.latte ← modelo para feed RSS +\-- + +Com o tempo, mais tipos de feed 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 + ├── amazon.latte ← feed para a Amazon + └── ebay.latte ← feed para eBay +\-- + +Essa transformação é totalmente tranquila - basta criar novas subpastas, dividir o código entre elas e atualizar os links (por exemplo, de `Export:feed` para `Export:Feed:amazon`). Graças a isso, podemos expandir gradualmente a estrutura conforme necessário, pois o nível de aninhamento não é limitado de forma alguma. + +Por exemplo, se na administração você tiver muitos apresentadores relacionados ao gerenciamento de pedidos, como `OrderDetail`, `OrderEdit`, `OrderDispatch` etc., poderá criar um módulo (pasta) `Order` para melhor organização, que conterá (pastas para) apresentadores `Detail`, `Edit`, `Dispatch` e outros. + + +Localização do modelo .[#toc-template-location] +----------------------------------------------- + +Nos exemplos anteriores, vimos que os modelos estão localizados diretamente na pasta com o apresentador: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← apresentador +├── DashboardTemplate.php ← classe de modelo opcional +└── default.latte ← modelo +\-- + +Esse local se mostra o mais conveniente na prática: você tem todos os arquivos relacionados à mão. + +Como alternativa, você pode colocar os modelos em uma subpasta `templates/`. O Nette oferece suporte a ambas as variantes. Você pode até mesmo colocar os modelos completamente fora da pasta `Presentation/`. Tudo sobre as opções de localização de modelos pode ser encontrado no capítulo [Pesquisa de modelos |templates#Template Lookup]. + + +Classes e componentes auxiliares .[#toc-helper-classes-and-components] +---------------------------------------------------------------------- + +Os apresentadores e modelos geralmente vêm com outros arquivos auxiliares. Nós os colocamos logicamente de acordo com seu escopo: + +1. **Diretamente com o apresentador** no caso de componentes específicos para um determinado apresentador: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← componente para listagem de produtos +└── FilterForm.php ← formulário para filtragem +\-- + +2. **Para o módulo** - recomendamos o uso da pasta `Accessory`, que é colocada ordenadamente no início do alfabeto: + +/--pre +Front/ +├── Accessory/ +│ ├── NavbarControl.php ← componentes para front-end +│ └── TemplateFilters.php +├── Product/ +└── Cart/ +\-- + +3. **Para todo o aplicativo** - 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 os componentes em `app/Components`. A escolha depende das convenções da equipe. + + +Modelo - Coração do aplicativo .[#toc-model-heart-of-the-application] +===================================================================== + +O modelo contém toda a lógica comercial do aplicativo. Para sua organização, aplica-se a mesma regra: 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 remessa +\-- + +No modelo, você normalmente encontra esses tipos de classes: + +**Facades**: representam o principal ponto de entrada em um domínio específico no aplicativo. Elas 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 fachada oculta os detalhes de implementação do restante do aplicativo, fornecendo assim uma interface limpa para trabalhar com o domínio em questão. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // validação + // criação de pedidos + // envio de e-mails + // gravação em estatísticas + } +} +``` + +**Serviços**: concentram-se em operações comerciais específicas em um domínio. Ao contrário das fachadas que orquestram casos de uso inteiros, um serviço implementa uma lógica comercial específica (como cálculos de preços ou processamento de pagamentos). Os serviços normalmente não têm estado e podem ser usados por fachadas como blocos de construção para operações mais complexas ou diretamente por outras partes do aplicativo para tarefas mais simples. + +```php +class PricingService +{ + public function calculateTotal(Order $order): Money + { + // Cálculo de preços + } +} +``` + +**Repositórios**: lidam com toda a comunicação com o armazenamento de dados, normalmente um banco de dados. Sua tarefa é carregar e salvar entidades e implementar métodos para pesquisá-las. Um repositório protege o restante do aplicativo 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 no aplicativo que têm sua identidade e mudam com o tempo. Normalmente, 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 pedidos do banco de dados +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, + ]); + } +} +``` + +**Objetos de valor**: objetos imutáveis que representam valores sem identidade própria - por exemplo, uma quantia em dinheiro ou um endereço de e-mail. Duas instâncias de um objeto de valor com os mesmos valores são consideradas idênticas. + + +Código de infraestrutura .[#toc-infrastructure-code] +==================================================== + +A pasta `Core/` (ou também `Infrastructure/`) abriga a base técnica do aplicativo. O código de infraestrutura normalmente inclui: + +/--pre +app/Core/ +├── Router/ ← roteamento e gerenciamento de URLs +│ └── RouterFactory.php +├── Security/ ← autenticação e autorização +│ ├── Authenticator.php +│ └── Authorizator.php +├── Logging/ ← registro e monitoramento +│ ├── SentryLogger.php +│ └── FileLogger.php +├── Cache/ ← camada de cache +│ └── FullPageCache.php +└── Integration/ ← integração com serviços externos + ├── Slack/ + └── Stripe/ +\-- + +Para projetos menores, uma estrutura plana é naturalmente suficiente: + +/--pre +Core/ +├── RouterFactory.php +├── Authenticator.php +└── QueueMailer.php +\-- + +Este é o código que: + +- Lida com a infraestrutura técnica (roteamento, registro, armazenamento em cache) +- Integra serviços externos (Sentry, Elasticsearch, Redis) +- Fornece serviços básicos para todo o aplicativo (correio, banco de dados) +- É, em grande parte, independente do domínio específico - o cache ou o registrador funciona da mesma forma para comércio eletrônico ou blog. + +Está se perguntando se uma determinada classe pertence a este site ou ao modelo? A principal diferença é que o código em `Core/`: + +- Não sabe nada sobre o domínio (produtos, pedidos, artigos) +- Geralmente pode ser transferido para outro projeto +- Resolve "como funciona" (como enviar e-mail), não "o que faz" (que e-mail enviar) + +Exemplo para melhor compreensão: + +- `App\Core\MailerFactory` - cria instâncias da classe de envio de e-mail, lida com as configurações de SMTP +- `App\Model\OrderMailer` - usa `MailerFactory` para enviar e-mails sobre pedidos, conhece seus modelos e quando eles devem ser enviados + + +Scripts de comando .[#toc-command-scripts] +========================================== + +Os aplicativos geralmente precisam executar tarefas fora das solicitações HTTP regulares, 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 real é 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 regulares + ├── NewsletterCommand.php ← envio de boletins informativos + └── ReminderCommand.php ← notificações de clientes +\-- + +O que pertence ao modelo e o que pertence aos scripts de comando? Por exemplo, a lógica para enviar um e-mail faz parte do modelo, enquanto o envio em massa de milhares de e-mails pertence a `Tasks/`. + +As tarefas geralmente são [executadas na linha de comando |https://blog.nette.org/en/cli-scripts-in-nette-application] ou via cron. Elas também podem ser executadas por meio de solicitação HTTP, mas a segurança deve ser considerada. O apresentador que executa a tarefa precisa ser protegido, por exemplo, somente para usuários conectados ou com um token forte e acesso a partir de endereços IP permitidos. Para tarefas longas, é necessário aumentar o limite de tempo do script e usar `session_write_close()` para evitar o bloqueio da sessão. + + +Outros diretórios possíveis .[#toc-other-possible-directories] +============================================================== + +Além dos diretórios básicos mencionados, você pode adicionar outras pastas especializadas de acordo com as necessidades do projeto. Vamos dar uma olhada nas mais comuns e em seu uso: + +/--pre +app/ +├── Api/ ← Lógica de API independente da camada de apresentação +├── Database/ ← scripts de migração e seeders para dados de teste +├── Components/ ← componentes visuais compartilhados em todo o aplicativo +├── Event/ ← útil se estiver usando arquitetura orientada por eventos +├── Mail/ ← modelos de e-mail e lógica relacionada +└── Utils/ ← classes auxiliares +\-- + +Para componentes visuais compartilhados usados em apresentadores em todo o aplicativo, você pode usar a pasta `app/Components` ou `app/Controls`: + +/--pre +app/Components/ +├── Form/ ← componentes de formulários compartilhados +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← componentes para listagens de dados +│ └── DataGrid.php +└── Navigation/ ← elementos de navegação + ├── Breadcrumbs.php + └── Menu.php +\-- + +É a essa pasta que pertencem os componentes com lógica mais complexa. Se quiser compartilhar componentes entre vários projetos, é bom separá-los em um pacote autônomo do composer. + +No diretório `app/Mail`, você pode colocar o gerenciamento de comunicação por e-mail: + +/--pre +app/Mail/ +├── templates/ ← modelos de e-mail +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Mapeamento de apresentadores .[#toc-presenter-mapping] +====================================================== + +O mapeamento define regras para derivar nomes de classes de nomes de apresentadores. Nós as especificamos na [configuração |configuration] sob a chave `application › mapping`. + +Nesta página, mostramos que colocamos os apresentadores na pasta `app/Presentation` (ou `app/UI`). Precisamos informar ao Nette sobre essa convenção no arquivo de configuração. Uma linha é suficiente: + +```neon +application: + mapping: App\Presentation\*\**Presenter +``` + +Como funciona o mapeamento? Para entender melhor, vamos primeiro imaginar um aplicativo sem módulos. Queremos que as classes de apresentador fiquem sob o namespace `App\Presentation`, de modo que o apresentador `Home` seja mapeado para a classe `App\Presentation\HomePresenter`. Isso é obtido com esta configuração: + +```neon +application: + mapping: App\Presentation\*Presenter +``` + +O mapeamento funciona substituindo o asterisco na máscara `App\Presentation\*Presenter` pelo nome do apresentador `Home`, resultando no nome final da classe `App\Presentation\HomePresenter`. Simples! + +Entretanto, como você pode ver nos exemplos deste e de outros capítulos, colocamos as classes de apresentador em subdiretórios homônimos, por exemplo, o apresentador `Home` mapeia para a classe `App\Presentation\Home\HomePresenter`. Conseguimos isso duplicando os dois pontos (requer o Nette Application 3.2): + +```neon +application: + mapping: App\Presentation\**Presenter +``` + +Agora, passaremos a mapear os apresentadores nos módulos. Podemos definir um mapeamento específico para cada módulo: + +```neon +application: + mapping: + Front: App\Presentation\Front\**Presenter + Admin: App\Presentation\Admin\**Presenter + Api: App\Api\*Presenter +``` + +De acordo com essa configuração, o apresentador `Front:Home` mapeia para a classe `App\Presentation\Front\Home\HomePresenter`, enquanto o apresentador `Api:OAuth` mapeia 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 desse tipo, é possível criar uma regra geral que os substituirá. Um novo asterisco para o módulo será adicionado à máscara de classe: + +```neon +application: + mapping: + *: App\Presentation\*\**Presenter + Api: App\Api\*Presenter +``` + +Isso também funciona para estruturas de diretório aninhadas mais profundas, como o apresentador `Admin:User:Edit`, em que o segmento com asterisco se repete para cada nível e resulta na classe `App\Presentation\Admin\User\Edit\EditPresenter`. + +Uma notação alternativa é usar uma matriz composta por três segmentos em vez de uma cadeia de caracteres. Essa 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 index 68f955b2b5..b63f1a692a 100644 --- a/application/pt/how-it-works.texy +++ b/application/pt/how-it-works.texy @@ -22,18 +22,18 @@ A estrutura do diretório é algo parecido com isto: /--pre web-project/ ├── app/ ← directory with application -│ ├── Presenters/ ← presenter classes -│ │ ├── HomePresenter.php ← Home presenter class -│ │ └── templates/ ← templates directory -│ │ ├── @layout.latte ← template of shared layout -│ │ └── Home/ ← templates for Home presenter -│ │ └── default.latte ← template for action `default` -│ ├── Router/ ← configuration of URL addresses +│ ├── Core/ ← classes básicas necessárias +│ │ └── RouterFactory.php ← configuração de endereços URL +│ ├── Presentation/ ← apresentadores, modelos e outros +│ │ ├── @layout.latte ← modelo de layout compartilhado +│ │ └── Home/ ← Diretório do apresentador Home +│ │ ├── HomePresenter.php ← Classe do apresentador da página inicial +│ │ └── default.latte ← template for action default │ └── Bootstrap.php ← booting class Bootstrap ├── bin/ ← scripts for the command line ├── config/ ← configuration files │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← error logs ├── temp/ ← temporary files, cache, … ├── vendor/ ← libraries installed by Composer @@ -45,9 +45,9 @@ A estrutura do diretório é algo parecido com isto: └── .htaccess ← prohibits access to all directories except www \-- -Você pode mudar a estrutura do diretório de qualquer forma, renomear ou mover pastas e depois basta editar os caminhos para `log/` e `temp/` no arquivo `Bootstrap.php` e o caminho para este arquivo em `composer.json` na seção `autoload`. Nada mais, nenhuma reconfiguração complicada, nenhuma mudança constante. A Nette tem uma [autodetecção inteligente |bootstrap#development-vs-production-mode]. +Você pode modificar a estrutura de diretórios como quiser, renomear ou mover pastas - é totalmente flexível. O Nette também apresenta detecção automática inteligente e reconhece automaticamente o local do aplicativo, incluindo sua base de URL. -Para aplicações um pouco maiores, podemos dividir pastas com apresentadores e modelos em subdiretórios (em disco) e em namespaces (em código), que chamamos de [módulos |modules]. +Para aplicativos um pouco maiores, podemos organizar as pastas do apresentador e do modelo em [subdiretórios |directory-structure#Presenters and templates] e agrupar as classes em namespaces, que chamamos de módulos. O diretório `www/` é o diretório público ou a base de documentos do projeto. Você pode renomeá-lo sem ter que definir nada mais no lado da aplicação. Basta [configurar a hospedagem |nette:troubleshooting#How to change or remove www directory from URL] para que a raíz do documento vá para este diretório. @@ -75,7 +75,7 @@ Sua tarefa é: Que tipo de fábrica? Nós não produzimos tratores, mas websites! Espere, isso será explicado imediatamente. -Por "inicializar o ambiente" queremos dizer, por exemplo, que [Tracy |tracy:] é ativado, o que é uma ferramenta incrível para registrar ou visualizar erros. Ele registra os erros no servidor de produção e os exibe diretamente no servidor de desenvolvimento. Portanto, a inicialização também precisa decidir se o site está rodando em modo de produção ou de desenvolvimento. Para isso, a Nette usa autodetecção: se você executar o site no localhost, ele roda em modo desenvolvedor. Você não precisa configurar nada e o aplicativo está pronto tanto para o desenvolvimento quanto para a implantação em produção. Estas etapas são executadas e descritas em detalhes no capítulo sobre a [classe Bootstrap |bootstrap]. +Por "inicialização do ambiente" queremos dizer, por exemplo, a ativação do [Tracy |tracy:], que é uma ferramenta fantástica para registro e visualização de erros. Nos servidores de produção, ele registra os erros, enquanto no desenvolvimento ele os exibe diretamente. Portanto, a inicialização inclui determinar se o site é executado no modo de produção ou de desenvolvimento. Para isso, o Nette usa [a detecção automática inteligente |bootstrap#development-vs-production-mode]: se você executar o site no localhost, ele funcionará no modo de desenvolvimento. Não é necessária nenhuma configuração e o aplicativo está pronto para a implantação de desenvolvimento e produção. Essas etapas são executadas e detalhadas no capítulo [da classe Bootstrap |bootstrap]. O terceiro ponto (sim, pulamos o segundo, mas voltaremos a ele) é iniciar a aplicação. O tratamento dos pedidos HTTP em Nette é feito pela classe `Nette\Application\Application` (doravante denominada `Application`), portanto, quando dizemos "executar uma aplicação", queremos chamar um método com o nome `run()` sobre um objeto desta classe. @@ -91,7 +91,7 @@ As aplicações escritas em Nette são divididas em muitos dos chamados apresent A aplicação começa pedindo ao chamado roteador que decida qual dos apresentadores deve passar o atual pedido de processamento. O roteador decide de quem é a responsabilidade. Ele analisa a URL de entrada `https://example.com/product/123`, que quer `show` um produto com `id: 123` como uma ação. É um bom hábito escrever um par de apresentador + ação separado por dois pontos como `Product:show`. -Assim, o roteador transformou a URL em um par `Presenter:action` + parâmetros, em nosso caso `Product:show` + `id: 123`. Você pode ver como é um roteador no arquivo `app/Router/RouterFactory.php` e nós o descreveremos em detalhes no capítulo [Roteamento |Routing]. +Assim, o roteador transformou a URL em um par `Presenter:action` + parâmetros, em nosso caso `Product:show` + `id: 123`. Você pode ver como é um roteador no arquivo `app/Core/RouterFactory.php` e nós o descreveremos em detalhes no capítulo [Roteamento |Routing]. Vamos em frente. O pedido já conhece o nome do apresentador e pode continuar. Ao criar um objeto `ProductPresenter`, que é o código do apresentador `Product`. Mais precisamente, pede ao contêiner DI para criar o apresentador, pois a produção de objetos é sua função. @@ -121,12 +121,9 @@ Então, o método `renderShow(123)` foi chamado, cujo código é um exemplo fict Posteriormente, o apresentador retorna a resposta. Pode ser uma página HTML, uma imagem, um documento XML, o envio de um arquivo do disco, JSON ou o redirecionamento para outra página. É importante ressaltar que se não dissermos explicitamente como responder (que é o caso de `ProductPresenter`), a resposta será renderizar o template com uma página HTML. Por quê? Bem, porque em 99% dos casos queremos desenhar um template, então o apresentador toma este comportamento como padrão e quer tornar nosso trabalho mais fácil. Esse é o ponto de vista da Nette. -Não temos nem mesmo que declarar qual modelo desenhar, ele deriva o caminho para ele de acordo com uma lógica simples. No caso do apresentador `Product` e da ação `show`, ele tenta ver se um destes arquivos de modelo existe em relação ao diretório onde se encontra a classe `ProductPresenter`: +Não precisamos nem mesmo especificar o modelo a ser renderizado; a estrutura deduzirá o caminho por si só. No caso da ação `show`, ele simplesmente tenta carregar o modelo `show.latte` no diretório com a classe `ProductPresenter`. Ele também tenta encontrar o layout no arquivo `@layout.latte` (mais informações sobre a [pesquisa de modelos |templates#Template Lookup]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -Ele também tentará encontrar o layout no arquivo `@layout.latte` e depois renderiza o modelo. Agora a tarefa do apresentador e de todo o pedido está concluída. Se o modelo não existir, uma página com erro 404 será devolvida. Você pode ler mais sobre os apresentadores na página [Apresentadores |Presenters]. +Em seguida, os modelos são renderizados. Isso conclui a tarefa do apresentador e de todo o aplicativo, e o trabalho está concluído. Se o modelo não existir, será retornada uma página de erro 404. Você pode ler mais sobre apresentadores na página [Apresentadores |presenters]. [* request-flow.svg *] @@ -137,7 +134,7 @@ Só para ter certeza, vamos tentar recapitular todo o processo com uma URL ligei 3) o roteador decodifica a URL como um par `Home:default` 4) um objeto `HomePresenter` é criado 5) método `renderDefault()` é chamado (se existir) -6) um modelo `templates/Home/default.latte` com um layout `templates/@layout.latte` é apresentado +6) um modelo `default.latte` com um layout `@layout.latte` é apresentado Você pode ter se deparado com muitos conceitos novos agora, mas acreditamos que eles fazem sentido. Criar aplicações em Nette é uma brisa. diff --git a/application/pt/modules.texy b/application/pt/modules.texy deleted file mode 100644 index 35ee3786d3..0000000000 --- a/application/pt/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Módulos -******* - -.[perex] -Em Nette, os módulos representam as unidades lógicas que compõem uma aplicação. Eles incluem apresentadores, gabaritos, possivelmente também componentes e classes de modelos. - -Um diretório para apresentadores e um para modelos não seria suficiente para projetos reais. Ter dezenas de arquivos em uma pasta é pelo menos desorganizado. Como sair dela? Nós simplesmente os dividimos em subdiretórios em disco e em namespaces no código. E é exatamente isso que os módulos Nette fazem. - -Portanto, vamos esquecer uma única pasta para apresentadores e modelos e, em vez disso, criar módulos, por exemplo `Admin` e `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← directory with modules -│ ├── Admin/ ← module Admin -│ │ ├── Presenters/ ← its presenters -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← module Front -│ └── Presenters/ ← its presenters -│ └── ... -\-- - -Esta estrutura de diretório será refletida pelos namespaces de classe, portanto, por exemplo `DashboardPresenter` estará no namespace `App\Modules\Admin\Presenters`: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -O `Dashboard` apresentador dentro do módulo `Admin` é referenciado dentro da aplicação usando a notação de cólon como `Admin:Dashboard`, e sua ação `default` como `Admin:Dashboard:default`. -E como a Nette propriamente dita sabe que `Admin:Dashboard` representa a classe `App\Modules\Admin\Presenters\DashboardPresenter`? Isto é determinado pelo [mapeamento |#mapping] na configuração. -Assim, a estrutura dada não é difícil de definir e você pode modificá-la de acordo com suas necessidades. - -Os módulos podem naturalmente conter todos os outros itens além de apresentadores e modelos, tais como componentes, classes de modelos, etc. - - -Módulos aninhados .[#toc-nested-modules] ----------------------------------------- - -Os módulos não precisam formar apenas uma estrutura plana, você também pode criar submódulos, por exemplo: - -/--pre -app/ -├── Modules/ ← directory with modules -│ ├── Blog/ ← module Blog -│ │ ├── Admin/ ← submodule Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← submodule Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← module Forum -│ │ └── ... -\-- - -Assim, o módulo `Blog` está dividido em `Admin` e `Front` submódulos. Mais uma vez, isto se refletirá nos namespaces, que serão `App\Modules\Blog\Admin\Presenters` etc. O apresentador `Dashboard` dentro do submódulo é referido como `Blog:Admin:Dashboard`. - -O ninho pode ir tão fundo quanto você desejar, de modo que sub-submódulos podem ser criados. - - -Criação de links .[#toc-creating-links] ---------------------------------------- - -Os links nos modelos de apresentadores são relativos ao módulo atual. Assim, o link `Foo:default` leva ao apresentador `Foo` no mesmo módulo que o apresentador atual. Se o módulo atual é `Front`, por exemplo, então o link vai assim: - -```latte -link to Front:Product:show -``` - -Um link é relativo mesmo que inclua o nome de um módulo, que é então considerado um submódulo: - -```latte -link to Front:Shop:Product:show -``` - -Links absolutos são escritos analogamente a caminhos absolutos em disco, mas com colons ao invés de cortes. Assim, uma ligação absoluta começa com dois-pontos: - -```latte -link to Admin:Product:show -``` - -Para saber se estamos em um determinado módulo ou em seu submódulo, podemos usar a função `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Roteiro .[#toc-routing] ------------------------ - -Ver [capítulo sobre roteamento |routing#Modules]. - - -Mapeamento .[#toc-mapping] --------------------------- - -Define as regras pelas quais o nome da classe é derivado do nome do apresentador. Nós as escrevemos na [configuração |configuration] sob a chave `application › mapping`. - -Vamos começar com uma amostra que não utiliza módulos. Queremos apenas que as classes de apresentadores tenham o namespace `App\Presenters`. Isso significa que um apresentador como o `Home` deve mapear para a classe `App\Presenters\HomePresenter`. Isto pode ser conseguido através da seguinte configuração: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -O nome do apresentador é substituído pelo asterisco na máscara da classe e o resultado é o nome da classe. Fácil! - -Se dividirmos os apresentadores em módulos, podemos ter nosso próprio mapeamento para cada módulo: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Agora o apresentador `Front:Home` mapeia para a classe `App\Modules\Front\Presenters\HomePresenter` e o apresentador `Admin:Dashboard` para a classe `App\Modules\Admin\Presenters\DashboardPresenter`. - -É mais prático criar uma regra geral (estrela) para substituir as duas primeiras. O asterisco extra será adicionado à máscara de classe apenas para o módulo: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Mas e se usarmos módulos aninhados e tivermos um apresentador `Admin:User:Edit`? Neste caso, o segmento com um asterisco representando o módulo para cada nível é simplesmente repetido e o resultado é a classe `App\Modules\Admin\User\Presenters\EditPresenter`. - -Uma notação alternativa é utilizar um conjunto composto de três segmentos em vez de um fio. Esta notação é equivalente à anterior: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -O valor padrão é `*: *Module\*Presenter`. diff --git a/application/pt/presenters.texy b/application/pt/presenters.texy index 58522bfda0..b6537f16c6 100644 --- a/application/pt/presenters.texy +++ b/application/pt/presenters.texy @@ -60,7 +60,7 @@ Semelhante ao método `render()`. Enquanto `render()` é destinado a É importante que `action()` é chamado antes `render()`Assim, dentro dele podemos possivelmente mudar o próximo ciclo de vida, ou seja, mudar o modelo que será apresentado e também o método `render()` que será chamado, usando `setView('otherView')`. -Os parâmetros do pedido 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 se não for um número inteiro, o apresentador retorna o [erro 404 |#Error 404 etc.] e encerra a operação. +Os parâmetros do pedido 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 se não for um número inteiro, o apresentador retorna o [erro 404 |#Error 404 etc.] e encerra a operação. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ No modelo, estas mensagens estão disponíveis na variável `$flashes` como obje Erro 404 etc. .[#toc-error-404-etc] =================================== -Quando não pudermos atender ao pedido porque, por exemplo, o artigo que queremos exibir não existe no banco de dados, vamos jogar fora o erro 404 usando o método `error(string $message = null, int $httpCode = 404)`, que representa o erro HTTP 404: +Quando não pudermos atender ao pedido porque, por exemplo, o artigo que queremos exibir não existe no banco de dados, vamos jogar fora o erro 404 usando o método `error(?string $message = null, int $httpCode = 404)`, que representa o erro HTTP 404: ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Parâmetros de solicitação .[#toc-request-parameters] +==================================================== + +O apresentador, assim como todos os componentes, obtém seus parâmetros da solicitação HTTP. Seus valores podem ser recuperados usando o método `getParameter($name)` ou `getParameters()`. Os valores são cadeias de caracteres ou matrizes de cadeias de caracteres, essencialmente dados brutos obtidos diretamente do URL. + +Para maior comodidade, recomendamos tornar os parâmetros acessíveis por meio de propriedades. Basta anotá-los com o atributo `#[Parameter]` atributo: + +```php +use Nette\Application\Attributes\Parameter; // essa linha é importante + +class HomePresenter extends Nette\Application\UI\Presenter +{ + #[Parameter] + public string $theme; // deve ser pública +} +``` + +Para propriedades, sugerimos especificar o tipo de dados (por exemplo, `string`). O Nette converterá automaticamente o valor com base nele. Os valores dos parâmetros também podem ser [validados |#Validation of Parameters]. + +Ao criar um link, você pode definir diretamente o valor dos parâmetros: + +```latte +click +``` + + Parâmetros Persistentes .[#toc-persistent-parameters] ===================================================== @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Se `$this->lang` tem um valor como `'en'`, então os links criados usando `link()` ou `n:href` também conterão o parâmetro `lang=en`. E quando o link for clicado, ele será novamente `$this->lang = 'en'`. -Para propriedades, recomendamos que você inclua o tipo de dados (por exemplo, `string`) e você também pode incluir um valor padrão. Os valores dos parâmetros podem ser [validados |#Validation of Persistent Parameters]. +Para propriedades, recomendamos que você inclua o tipo de dados (por exemplo, `string`) e também pode incluir um valor padrão. Os valores dos parâmetros podem ser [validados |#Validation of Parameters]. Parâmetros persistentes são passados por padrão entre todas as ações de um determinado apresentador. Para passá-los entre vários apresentadores, você precisa defini-los também: @@ -307,18 +333,12 @@ Indo mais fundo .[#toc-going-deeper] O que mostramos até agora neste capítulo provavelmente será suficiente. As seguintes linhas destinam-se àqueles que estão interessados em apresentadores em profundidade e querem saber tudo. -Requisitos e parâmetros .[#toc-requirement-and-parameters] ----------------------------------------------------------- +Validação de parâmetros .[#toc-validation-of-parameters] +-------------------------------------------------------- -O pedido tratado pelo apresentador é o objeto [api:Nette\Application\Request] e é devolvido pelo método do apresentador `getRequest()`. Ele inclui um conjunto de parâmetros e cada um deles pertence a alguns dos componentes ou diretamente ao apresentador (que na verdade é também um componente, embora especial). Assim, Nette redistribui os parâmetros e passa entre os componentes individuais (e o apresentador), chamando o método `loadState(array $params)`. Os parâmetros podem ser obtidos pelo método `getParameters(): array`, individualmente usando `getParameter($name)`. Os valores dos parâmetros são strings ou matrizes de strings, são basicamente dados brutos obtidos diretamente de uma URL. +Os valores dos [parâmetros de solicitação |#request parameters] e dos [parâmetros persistentes |#persistent parameters] recebidos dos URLs são gravados nas propriedades pelo método `loadState()`. Ele também verifica se o tipo de dados especificado na propriedade corresponde; caso contrário, ele responderá com um erro 404 e a página não será exibida. - -Validação de Parâmetros Persistentes .[#toc-validation-of-persistent-parameters] --------------------------------------------------------------------------------- - -Os valores de [parâmetros persistentes |#persistent parameters] recebidos de URLs 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, responderá com um erro 404 e a página não será exibida. - -Nunca confie cegamente em parâmetros persistentes, pois eles podem ser facilmente sobrescritos pelo usuário no URL. Por exemplo, é assim que verificamos se `$this->lang` está entre os idiomas suportados. Uma boa maneira de fazer isso é sobrescrever o método `loadState()` mencionado acima: +Nunca confie cegamente nos parâmetros, pois eles podem ser facilmente substituídos pelo usuário no URL. Por exemplo, é assim que verificamos se `$this->lang` está entre os idiomas suportados. Uma boa maneira de fazer isso é substituir o método `loadState()` mencionado acima: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,7 +361,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Salvar e Restaurar o Pedido .[#toc-save-and-restore-the-request] ---------------------------------------------------------------- -Você pode salvar o pedido atual para uma sessão ou restaurá-lo da sessão e deixar o apresentador executá-lo novamente. Isto é útil, por exemplo, quando um usuário preenche um formulário e seu login expira. Para não perder dados, antes de redirecionar para a página de login, salvamos a solicitação atual para a sessão usando `$reqId = $this->storeRequest()`, que retorna um identificador na forma de uma seqüência curta e o passa como parâmetro para o apresentador de login. +A solicitação que o apresentador trata é um objeto [api:Nette\Application\Request] e é retornada pelo método do apresentador `getRequest()`. + +Você pode salvar a solicitação atual em uma sessão ou restaurá-la da sessão e permitir que o apresentador a execute novamente. Isso é útil, por exemplo, quando um usuário preenche um formulário e seu login expira. Para não perder dados, antes de redirecionar para a página de login, salvamos a solicitação atual na sessão usando `$reqId = $this->storeRequest()`, que retorna um identificador na forma de uma cadeia de caracteres curta e o passa como parâmetro para o apresentador de login. Após o login, chamamos o método `$this->restoreRequest($reqId)`, que capta o pedido da sessão e o encaminha para ele. O método verifica que a solicitação foi criada pelo mesmo usuário que está agora logado. Se outro usuário faz o login ou a chave é inválida, ele não faz nada e o programa continua. @@ -362,7 +384,7 @@ O redirecionamento não ocorre com um pedido AJAX ou POST porque resultaria em p Você também pode invocar a canonização manualmente usando o método `canonicalize()`, que, como o método `link()`, recebe o apresentador, ações e parâmetros como argumentos. Ele cria um link e o compara com a URL atual. Se for diferente, ele se redireciona para o link gerado. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // redireciona se $slug for diferente de $realSlug @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Restrição de acesso usando `#[Requires]` .[#toc-access-restriction-using-requires]{data-version:3.2.2} +------------------------------------------------------------------------------------------------------ + +O atributo `#[Requires]` fornece opções avançadas para restringir o acesso aos apresentadores e seus métodos. Ele pode ser usado para especificar métodos HTTP, exigir solicitações AJAX, limitar o acesso à mesma origem e restringir o acesso somente ao encaminhamento. O atributo pode ser aplicado a classes de apresentadores, bem como a métodos individuais, como `action()`, `render()`, `handle()`, e `createComponent()`. + +Você pode especificar essas restrições: +- em métodos HTTP: `#[Requires(methods: ['GET', 'POST'])]` +- que exigem uma solicitação AJAX: `#[Requires(ajax: true)]` +- acesso somente a partir da mesma origem: `#[Requires(sameOrigin: true)]` +- acesso somente por meio de encaminhamento: `#[Requires(forward: true)]` +- restrições a ações específicas: `#[Requires(actions: 'default')]` + +Para obter detalhes, consulte [Como usar o atributo Requires |best-practices:attribute-requires]. + + +Verificação do método HTTP .[#toc-http-method-check] +---------------------------------------------------- + +No Nette, os apresentadores verificam automaticamente o método HTTP de cada solicitação recebida, principalmente por motivos de segurança. Por padrão, os métodos `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` são permitidos. + +Se quiser habilitar métodos adicionais, como `OPTIONS`, você pode usar o atributo `#[Requires]` (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 é realizada em `checkHttpMethod()`, que verifica se o método especificado na solicitação está incluído na matriz `$presenter->allowedMethods`. Adicione um método como este: + +```php +class MyPresenter extends Nette\Application\UI\Presenter +{ + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } +} +``` + +É fundamental enfatizar que, se você habilitar o método `OPTIONS`, também deverá tratá-lo adequadamente em seu apresentador. Esse método é frequentemente usado como a chamada solicitação de preflight, que os navegadores enviam automaticamente antes da solicitação real quando é necessário determinar se a solicitação é permitida do ponto de vista da política de CORS (Cross-Origin Resource Sharing). Se você permitir esse método, mas não implementar uma resposta adequada, isso poderá gerar inconsistências e possíveis problemas de segurança. + + Leitura adicional .[#toc-further-reading] ========================================= diff --git a/application/pt/routing.texy b/application/pt/routing.texy index 6caf887e2d..aca6582ea9 100644 --- a/application/pt/routing.texy +++ b/application/pt/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Ou podemos usar este formulário, observando a reescrita da expressão regular de validação: +Para uma especificação mais detalhada, é possível usar um formulário ainda mais extenso, no qual, além dos valores padrão, outras propriedades de parâmetro podem ser definidas, como uma expressão regular de validação (consulte o parâmetro `id` ): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Estes formatos mais faladores são úteis para adicionar outros metadados. +É importante observar que, se os parâmetros definidos na matriz não estiverem incluídos na máscara de caminho, seus valores não poderão ser alterados, nem mesmo usando parâmetros de consulta especificados após um ponto de interrogação no URL. Filtros e Traduções .[#toc-filters-and-translations] @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Módulos .[#toc-modules] ----------------------- -Se temos mais rotas que pertencem a um [módulo |modules], podemos usar `withModule()` para agrupá-las: +Se tivermos mais rotas que pertençam a um [módulo |directory-structure#Presenters and Templates], poderemos usar `withModule()` para agrupá-las: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Integração .[#toc-integration] ============================== -A fim de conectar nosso roteador à aplicação, devemos informar o recipiente DI sobre isso. A maneira mais fácil é preparar a fábrica que irá construir o objeto roteador e dizer à configuração do contêiner para usá-lo. Portanto, digamos que escrevemos um método para este fim `App\Router\RouterFactory::createRouter()`: +A fim de conectar nosso roteador à aplicação, devemos informar o recipiente DI sobre isso. A maneira mais fácil é preparar a fábrica que irá construir o objeto roteador e dizer à configuração do contêiner para usá-lo. Portanto, digamos que escrevemos um método para este fim `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ Em seguida, escrevemos em [configuração |dependency-injection:services]: ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Quaisquer dependências, tais como uma conexão de banco de dados, etc., são passadas para o método de fábrica como seus parâmetros usando [a fiação automática |dependency-injection:autowiring]: @@ -663,7 +663,7 @@ Por uso separado, entendemos o uso das capacidades do roteador em uma aplicaçã Assim, mais uma vez, criaremos um método que construirá um roteador, por exemplo: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Ou criaremos objetos diretamente: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/pt/templates.texy b/application/pt/templates.texy index 059e35178e..c8b0efb3c0 100644 --- a/application/pt/templates.texy +++ b/application/pt/templates.texy @@ -34,35 +34,81 @@ E este poderia ser o modelo de ação: Ele define o bloco `content`, que é inserido no lugar de `{include content}` no layout, e também redefine o bloco `title`, que sobrescreve `{block title}` no layout. Tente imaginar o resultado. -Busca de modelos .[#toc-search-for-templates] ---------------------------------------------- +Pesquisa de modelos .[#toc-template-lookup] +------------------------------------------- -O caminho para os modelos é deduzido de acordo com uma lógica simples. Ele tenta ver se um destes arquivos de gabaritos existe em relação ao diretório onde se encontra a classe apresentadora, onde `` é o nome do atual apresentador e `` é o nome da ação atual: +Nos apresentadores, você não precisa especificar qual modelo deve ser renderizado; a estrutura determinará automaticamente o caminho, facilitando a codificação para você. -- `templates//.latte` -- `templates/..latte` +Se você usar uma estrutura de diretórios em que cada apresentador tenha seu próprio diretório, basta colocar o modelo nesse diretório com o nome da ação (ou seja, visualização). Por exemplo, para a ação `default`, use o modelo `default.latte`: -Se o modelo não for encontrado, ele tentará procurar no diretório `templates` um nível acima, ou seja, no mesmo nível que o diretório com a classe apresentadora. +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -Se o modelo também não for encontrado lá, a resposta é um [erro 404 |presenters#Error 404 etc.]. +Se você usar uma estrutura em que os apresentadores estejam juntos em um diretório e os modelos em uma pasta `templates`, salve-os em um arquivo `..latte` ou `/.latte`: -Você também pode mudar a visão usando `$this->setView('otherView')`. Ou, em vez de procurar, especifique diretamente o nome do arquivo modelo usando `$this->template->setFile('/path/to/template.latte')`. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- + +O diretório `templates` também pode ser colocado um nível acima, no mesmo nível do diretório com as classes de apresentador. + +Se o modelo não for encontrado, o apresentador responderá com o [erro 404 - página não encontrada |presenters#Error 404 etc]. + +Você pode alterar a visualização usando `$this->setView('anotherView')`. Também é possível especificar diretamente o arquivo de modelo com `$this->template->setFile('/path/to/template.latte')`. .[note] -Você pode alterar os caminhos onde os modelos são pesquisados substituindo o método de [formataçãoTemplateFiles |api:Nette\Application\UI\Presenter::formatTemplateFiles()], que retorna um conjunto de possíveis caminhos de arquivos. +Os arquivos onde os modelos são pesquisados podem ser alterados substituindo o método [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], que retorna uma matriz de possíveis nomes de arquivos. + + +Pesquisa de modelos de layout .[#toc-layout-template-lookup] +------------------------------------------------------------ + +O Nette também procura automaticamente o arquivo de layout. + +Se você usar uma estrutura de diretórios em que cada apresentador tenha seu próprio diretório, coloque o layout na pasta com o apresentador, se for específico apenas para ele, ou em um nível superior, se for comum a vários apresentadores: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +Se você usar uma estrutura em que os apresentadores estejam agrupados em um diretório e os modelos estejam em uma pasta `templates`, o layout será esperado nos seguintes locais: -O layout é esperado nos seguintes arquivos: +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` layout comum a vários apresentadores +Se o apresentador estiver em um módulo, ele também pesquisará mais acima na árvore de diretórios, de acordo com o aninhamento do módulo. -`` é o nome do atual apresentador e `` é o nome do layout, que é, por padrão, `'layout'`. O nome pode ser alterado com `$this->setLayout('otherLayout')`, para que os arquivos `@otherLayout.latte` sejam experimentados. +O nome do layout pode ser alterado usando `$this->setLayout('layoutAdmin')` e, em seguida, ele será esperado no arquivo `@layoutAdmin.latte`. Você também pode especificar diretamente o arquivo de modelo de layout usando `$this->setLayout('/path/to/template.latte')`. -Você também pode especificar diretamente o nome do arquivo do modelo de layout usando `$this->setLayout('/path/to/template.latte')`. O uso do `$this->setLayout(false)` desabilitará a busca do layout. +O uso de `$this->setLayout(false)` ou da tag `{layout none}` dentro do modelo desativa a pesquisa de layout. .[note] -Você pode alterar os caminhos onde os modelos são pesquisados, substituindo o método [formatLayoutTemplateFiles |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], que retorna uma série de possíveis caminhos de arquivos. +Os arquivos em que os modelos de layout são pesquisados podem ser alterados substituindo o método [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], que retorna uma matriz de possíveis nomes de arquivos. Variáveis no modelo .[#toc-variables-in-the-template] @@ -104,7 +150,7 @@ A anotação `@property-read` é para IDE e análise estática, fará com que o Você também pode se dar ao luxo de sussurrar nos modelos, basta instalar o plugin Latte no PhpStorm e especificar o nome da classe no início do modelo, veja o artigo "Latte: como digitar sistema":https://blog.nette.org/pt/latte-como-usar-o-sistema-de-tipo: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void A versão 3 do Latte oferece uma maneira mais avançada, criando uma [extensão |latte:creating-extension] para cada projeto web. Aqui está um exemplo rudimentar de tal classe: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ Registramos usando [a configuração#Latte |configuration#Latte]: ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ Alternativamente, o tradutor pode ser definido usando a [configuração |configu ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` O tradutor pode então ser usado, por exemplo, como um filtro `|translate`, com parâmetros adicionais passados para o método `translate()` (ver `foo, bar`): diff --git a/application/ro/@home.texy b/application/ro/@home.texy index 8a8c11fbf4..a5428fbadd 100644 --- a/application/ro/@home.texy +++ b/application/ro/@home.texy @@ -1,36 +1,85 @@ -Aplicație Nette -*************** +Cerere Nette +************ .[perex] -Pachetul `nette/application` este baza pentru crearea de aplicații web interactive. +Nette Application este nucleul cadrului Nette care aduce instrumente puternice pentru crearea de aplicații web moderne. Acesta oferă numeroase caracteristici excepționale care simplifică semnificativ dezvoltarea și îmbunătățesc securitatea și mentenabilitatea codului. -- [Cum funcționează aplicațiile? |how-it-works] -- [Bootstrap |Bootstrap] -- [Prezentatori |Presenters] -- [Șabloane |Templates] -- [Module |Modules] -- [Rutarea |Routing] -- [Crearea de linkuri URL |creating-links] -- [Componente interactive |components] -- [AJAX & Snippets |ajax] -- [Multiplicator |multiplier] -- [Configurație |Configuration] +Instalare .[#toc-installation] +------------------------------ -Instalare ---------- - -Descărcați și instalați pachetul folosind [Composer |best-practices:composer]: +Descărcați și instalați biblioteca utilizând [Composer |best-practices:composer]: ```shell composer require nette/application ``` + +De ce să alegeți Nette Application? .[#toc-why-choose-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 genera URL-uri și în sens invers. Aceasta înseamnă că: +- Puteți modifica oricând structura URL a întregii aplicații fără a modifica fișierele șablon +- URL-urile sunt canonicalizate automat, îmbunătățind 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 unic printre framework-urile PHP: +- Permite crearea de elemente UI reutilizabile +- Suportă compoziția ierarhică a componentelor +- Oferă o gestionare elegantă a cererilor AJAX utilizând semnale +- Bibliotecă bogată de componente gata făcute pe [Componette](https://componette.org) + +**AJAX și Snippets:** Nette a introdus un mod revoluționar de a lucra cu AJAX în 2009, înainte de soluții precum Hotwire pentru Ruby on Rails sau Symfony UX Turbo: +- Snippets permit actualizarea doar a unor părți ale paginii fără a scrie JavaScript +- Integrare automată cu sistemul de componente +- invalidarea inteligentă a secțiunilor paginii +- Transfer minim de date + +**Intuitive [Latte |latte:] Templates:** Cel mai sigur sistem de template-uri pentru PHP cu caracteristici avansate: +- Protecție XSS automată cu scăpare sensibilă la context +- Extensibil cu filtre, funcții și etichete personalizate +- Moștenirea șabloanelor și fragmente pentru AJAX +- Suport excelent pentru PHP 8.x cu sistem de tipuri + +**Dependency Injection:** Nette utilizează pe deplin Dependency Injection: +- Trecerea automată a dependențelor (autowiring) +- Configurare utilizând formatul clar NEON +- Suport pentru fabricile de componente + + +Principalele beneficii .[#toc-main-benefits] +-------------------------------------------- + +- **Securitate**: Protecție automată împotriva [vulnerabilităților |nette:vulnerability-protection] precum XSS, CSRF, etc. +- **Productivitate**: Mai puțină scriere, mai multe funcții datorită designului inteligent +- **Debugging**: [Tracy debugger |tracy:] cu panou de rutare +- **Performanță**: Sistem inteligent de caching, încărcare leneșă a componentelor +- **Flexibilitate**: Modificarea ușoară a URL-ului chiar și după finalizarea aplicației +- **Componente**: Sistem unic de elemente UI reutilizabile +- **Modern**: Suport complet pentru PHP 8.4+ și sistemul de tipuri + + +Noțiuni introductive .[#toc-getting-started] +-------------------------------------------- + +1. [Înțelegerea aplicațiilor |how-it-works] - Înțelegerea arhitecturii de bază +2. [Prezentatori |presenters] - Lucrul cu prezentatorii și acțiunile +3. [Șabloane |templates] - Crearea de șabloane în Latte +4. [Rutare |routing] - Configurarea URL +5. [Componente interactive |components] - Utilizarea sistemului de componente + + +Compatibilitatea PHP .[#toc-php-compatibility] +---------------------------------------------- + | versiune | compatibil cu PHP |-----------|------------------- -| Nette Application 4.0 | PHP 8.0 - 8.2 -| Nette Application 3.1 | PHP 7.2 - 8.2 -| Nette Application 3.0 | PHP 7.1 - 8.0 -| Nette Application 2.4 | PHP 5.6 - 8.0 +| Aplicația Nette 4.0 | PHP 8.1 - 8.4 +| Aplicația Nette 3.2 | PHP 8.1 - 8.4 +| Nette Application 3.1 | PHP 7.2 - 8.3 +| Aplicația Nette 3.0 | PHP 7.1 - 8.0 +| Aplicație Nette 2.4 | PHP 5.6 - 8.0 -Se aplică la cele mai recente versiuni de patch-uri. +Valabil pentru cele mai recente versiuni de patch-uri. \ No newline at end of file diff --git a/application/ro/@left-menu.texy b/application/ro/@left-menu.texy index 97c4913b85..66ebac6740 100644 --- a/application/ro/@left-menu.texy +++ b/application/ro/@left-menu.texy @@ -4,7 +4,7 @@ Aplicație Nette - [Bootstrap |Bootstrap] - [Prezentatori |Presenters] - [Șabloane |Templates] -- [Module |Modules] +- [Structura directorului |directory-structure] - [Rutarea |Routing] - [Crearea de linkuri URL |creating-links] - [Componente interactive |components] diff --git a/application/ro/ajax.texy b/application/ro/ajax.texy index 145ab87fdd..03eec3fac5 100644 --- a/application/ro/ajax.texy +++ b/application/ro/ajax.texy @@ -3,10 +3,10 @@ AJAX & Snippets
    -În prezent, aplicațiile web moderne rulează pe jumătate pe un server și pe jumătate în browser. AJAX este un factor vital de unificare. Ce suport oferă Nette Framework? -- trimiterea de fragmente de șabloane (așa-numitele *snippets*) +În era aplicațiilor web moderne, în care funcționalitatea se întinde adesea între server și browser, AJAX este un element de legătură esențial. Ce opțiuni oferă Nette Framework în acest domeniu? +- trimiterea unor părți din șablon, așa-numitele snippet-uri - transmiterea de variabile între PHP și JavaScript -- depanarea aplicațiilor AJAX +- instrumente de depanare a cererilor AJAX
    @@ -14,29 +14,32 @@ AJAX & Snippets Cerere AJAX .[#toc-ajax-request] ================================ -O cerere AJAX nu diferă de o cerere clasică - prezentatorul este apelat cu o vizualizare și parametri specifici. De asemenea, este la latitudinea prezentatorului cum să răspundă la aceasta: poate utiliza propria rutină, care returnează un fragment de cod HTML (fragment HTML), un document XML, un obiect JSON sau cod JavaScript. +O cerere AJAX nu diferă în mod fundamental de o cerere HTTP clasică. Un prezentator este apelat cu parametri specifici. Depinde de prezentator cum să răspundă la cerere - acesta poate returna date în format JSON, poate trimite o parte din codul HTML, un document XML etc. -Pe partea serverului, o cerere AJAX poate fi detectată cu ajutorul metodei de serviciu care [încapsulează cererea HTTP |http:request] `$httpRequest->isAjax()` (detectează pe baza antetului HTTP `X-Requested-With`). În interiorul prezentatorului, este disponibilă o scurtătură sub forma metodei `$this->isAjax()`. +Pe partea de browser, inițiem o cerere AJAX utilizând funcția `fetch()`: -Există un obiect preprocesat numit `payload` dedicat trimiterii de date către browser în JSON. - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // prelucrarea răspunsului +}); ``` -Pentru un control complet asupra ieșirii JSON, utilizați metoda `sendJson` în prezentator. Aceasta termină imediat presenterul și vă veți descurca fără șablon: +Pe partea serverului, o cerere AJAX este recunoscută de metoda `$httpRequest->isAjax()` a serviciului [care încapsulează cererea HTTP |http:request]. Aceasta utilizează antetul HTTP `X-Requested-With`, deci este esențial să o trimitem. În cadrul prezentatorului, puteți utiliza metoda `$this->isAjax()`. + +Dacă doriți să trimiteți date în format JSON, utilizați metoda [`sendJson()` |presenters#Sending a response] metoda . Metoda încheie, de asemenea, activitatea prezentatorului. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Dacă dorim să trimitem HTML, putem fie să setăm un șablon special pentru cererile AJAX: +Dacă intenționați să răspundeți cu un șablon special conceput pentru AJAX, puteți face acest lucru după cum urmează: ```php public function handleClick($param): void @@ -44,27 +47,43 @@ public function handleClick($param): void if ($this->isAjax()) { $this->template->setFile('path/to/ajax.latte'); } - // ... + //... } ``` +Fragmente .[#toc-snippets] +========================== + +Cel mai puternic instrument oferit de Nette pentru conectarea serverului cu clientul sunt snippets. Cu ajutorul acestora, puteți transforma o aplicație obișnuită într-una AJAX cu un efort minim și câteva linii de cod. Exemplul Fifteen demonstrează cum funcționează totul, iar codul său poate fi găsit pe [GitHub |https://github.com/nette-examples/fifteen]. + +Snippets, sau clippings, vă permit să actualizați doar părți ale paginii, în loc să reîncărcați întreaga pagină. Acest lucru este mai rapid și mai eficient și oferă, de asemenea, o experiență mai confortabilă pentru utilizator. Snippets s-ar putea să vă amintească 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? Atunci când pagina este încărcată pentru prima dată (o cerere non-AJAX), se încarcă întreaga pagină, inclusiv toate snippet-urile. Atunci când utilizatorul interacționează cu pagina (de exemplu, face clic pe un buton, trimite un formular etc.), în loc să se încarce întreaga pagină, se face o cerere AJAX. Codul din prezentator execută acțiunea și decide ce fragmente trebuie actualizate. Nette redă aceste fragmente și le trimite sub forma unei matrice JSON. Codul de manipulare din browser inserează apoi fragmentele primite înapoi în pagină. Prin urmare, este transferat doar codul fragmentelor modificate, ceea ce permite economisirea lățimii de bandă și accelerarea încărcării în comparație cu transferul întregului conținut al paginii. + + Naja .[#toc-naja] -================= +----------------- - [Librăria Naja |https://naja.js.org] este utilizată pentru a gestiona cererile AJAX din partea browserului. [Instalați-o |https://naja.js.org/#/guide/01-install-setup-naja] ca un pachet node.js (pentru a o utiliza cu Webpack, Rollup, Vite, Parcel și altele): +Pentru a gestiona fragmente de text în browser, se utilizează [biblioteca Naja |https://naja.js.org]. [Instalați-o |https://naja.js.org/#/guide/01-install-setup-naja] ca un pachet node.js (pentru a fi utilizată cu aplicații precum Webpack, Rollup, Vite, Parcel și altele): ```shell npm install naja ``` -...sau inserați-o direct în șablonul de pagină: +... sau inserați-o direct în șablonul de pagină: ```html ``` -Pentru a crea o solicitare AJAX dintr-un link obișnuit (semnal) sau un formular de trimitere, trebuie doar să marcați link-ul, formularul sau butonul respectiv cu clasa `ajax`: +Mai întâi trebuie 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 un formular de trimitere într-o cerere AJAX, este suficient să marcați link-ul, formularul sau butonul respectiv cu clasa `ajax`: ```html Go @@ -74,64 +93,39 @@ Pentru a crea o solicitare AJAX dintr-un link obișnuit (semnal) sau un formular or +
    ``` -Snippets .[#toc-snippets] -========================= - -Există un instrument mult mai puternic de suport AJAX încorporat - snippets. Utilizarea acestora face posibilă transformarea unei aplicații obișnuite într-una AJAX folosind doar câteva linii de cod. Modul în care funcționează totul este demonstrat în exemplul Fifteen, al cărui cod este, de asemenea, accesibil în build sau pe [GitHub |https://github.com/nette-examples/fifteen]. - -Modul în care funcționează snippet-urile este că întreaga pagină este transferată în timpul cererii inițiale (adică non-AJAX) și apoi, la fiecare [subcerere |components#signal] AJAX (cerere a aceleiași vizualizări a aceluiași prezentator), doar codul părților modificate este transferat în depozitul `payload` menționat anterior. - -Snippets vă poate aminti de Hotwire pentru Ruby on Rails sau de Symfony UX Turbo, dar Nette a venit cu ele cu paisprezece ani mai devreme. - - -Invalidarea Snippets .[#toc-invalidation-of-snippets] -===================================================== +Redesenarea fragmentelor .[#toc-redrawing-snippets] +--------------------------------------------------- -Fiecare descendent al clasei [Control |components] (care este, de asemenea, un prezentator) este capabil să își amintească dacă au existat modificări în timpul unei solicitări care să necesite o nouă redare. Există o pereche de metode pentru a gestiona acest lucru: `redrawControl()` și `isControlInvalid()`. Un exemplu: +Fiecare obiect al clasei [Control |components] (inclusiv prezentatorul însuși) păstrează o evidență a modificărilor care au avut loc și care necesită redesenarea sa. Metoda `redrawControl()` este utilizată în acest scop. ```php public function handleLogin(string $user): void { - // Obiectul trebuie să se redea după ce utilizatorul s-a logat + // după logare, este necesar să se redeseneze partea relevantă $this->redrawControl(); - // ... + //... } ``` -Nette oferă însă o rezoluție și mai fină decât componentele întregi. Metodele enumerate acceptă ca parametru opțional numele unui așa-numit "snippet". Un "snippet" este practic un element din șablonul dvs. marcat în acest scop de o etichetă Latte, mai multe detalii în continuare. Astfel, este posibil să cereți unei componente să redeseneze doar *părți* din șablonul său. Dacă întreaga componentă este invalidată, atunci toate "snippet-urile" sale sunt reredimensionate. O componentă este "invalidată" și dacă oricare dintre subcomponentele sale este invalidată. -```php -$this->isControlInvalid(); // -> fals - -$this->redrawControl('header'); // invalidează fragmentul numit "header -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, cel puțin un fragment este invalidat +Nette permite, de asemenea, un control mai fin al elementelor care trebuie redesenate. Metoda menționată mai sus poate primi ca argument numele fragmentului. Astfel, este posibil să se invalideze (adică să se forțeze o redesenare) la nivelul părții de șablon. În cazul în care întreaga componentă este invalidată, fiecare fragment din aceasta este, de asemenea, redesenat: -$this->redrawControl(); // invalidează întreaga componentă, fiecare fragment -$this->isControlInvalid('footer'); // -> true +```php +// invalidează fragmentul de "antet +$this->redrawControl('header'); ``` -O componentă care primește un semnal este marcată automat pentru redesenare. - -Mulțumită redimensionării fragmentelor, știm exact ce părți din ce elemente trebuie redimensionate. - -Etichetă `{snippet} … {/snippet}` .{toc: Tag snippet} -===================================================== +Fragmente în Latte .[#toc-snippets-in-latte] +-------------------------------------------- -Redarea paginii se desfășoară în mod similar cu o cerere obișnuită: sunt încărcate aceleași șabloane etc. Partea vitală este, totuși, să se lase deoparte părțile care nu trebuie să ajungă la ieșire; celelalte părți trebuie asociate cu un identificator și trimise utilizatorului într-un format inteligibil pentru un manipulator JavaScript. - - -Sintaxa .[#toc-syntax] ----------------------- - -Dacă există un control sau un fragment în șablon, trebuie să îl înfășurăm cu ajutorul etichetei `{snippet} ... {/snippet}` - aceasta se va asigura că fragmentul redat va fi "tăiat" și trimis către browser. De asemenea, acesta va fi inclus într-un helper `
    ` (este posibil să se folosească o altă etichetă). În exemplul următor este definit un fragment numit `header`. Acesta poate reprezenta la fel de bine șablonul unei componente: +Utilizarea snippet-urilor în Latte este extrem de ușoară. Pentru a defini o parte a șablonului ca fiind un snippet, este suficient să o înfășurați în etichetele `{snippet}` și `{/snippet}`: ```latte {snippet header} @@ -139,7 +133,9 @@ Dacă există un control sau un fragment în șablon, trebuie să îl înfășur {/snippet} ``` -Un fragment de alt tip decât `
    ` sau un fragment cu atribute HTML suplimentare se obține prin utilizarea variantei de atribut: +Snippet-ul creează un element `
    ` în pagina HTML cu o etichetă special generată `id`. Atunci când se redesenează un fragment, conținutul acestui element este actualizat. Prin urmare, atunci când pagina este redată inițial, toate fragmentele trebuie, de asemenea, redate, chiar dacă acestea pot fi inițial goale. + +De asemenea, puteți crea un fragment cu un alt element decât `
    ` utilizând un atribut n:: ```latte
    @@ -148,138 +144,106 @@ Un fragment de alt tip decât `
    ` sau un fragment cu atribute HTML supliment ``` -Snippets dinamice .[#toc-dynamic-snippets] -========================================== +Zone de fragmente .[#toc-snippet-areas] +--------------------------------------- -În Nette puteți defini, de asemenea, fragmente cu un nume dinamic bazat pe un parametru de execuție. Acest lucru este cel mai potrivit pentru diverse liste în cazul în care trebuie să modificăm doar un singur rând, dar nu dorim să transferăm întreaga listă odată cu el. Un exemplu în acest sens ar fi: +Numele snippet-urilor pot fi, de asemenea, expresii: ```latte -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Există un fragment static numit `itemsContainer`, care conține mai multe fragmente dinamice: `item-0`, `item-1` și așa mai departe. +În acest fel, vom obține mai multe fragmente precum `item-0`, `item-1`, etc. Dacă ar fi să invalidăm direct un fragment dinamic (de exemplu, `item-1`), nimic nu ar fi redesenat. Motivul este că snippet-urile funcționează ca adevărate extrase și doar ele însele sunt redate direct. Cu toate acestea, în șablon, nu există, din punct de vedere tehnic, un fragment numit `item-1`. Acesta apare doar atunci când se execută codul care înconjoară fragmentul, în acest caz, bucla foreach. Prin urmare, vom marca partea din șablon care trebuie executată cu eticheta `{snippetArea}`: -Nu puteți redesena direct un fragment dinamic (redesenarea lui `item-1` nu are niciun efect), ci trebuie să redesenați fragmentul său părinte (în acest exemplu `itemsContainer`). Acest lucru determină executarea codului snippet-ului părinte, dar apoi doar sub-snippet-urile sale sunt trimise către browser. Dacă doriți să trimiteți doar unul dintre sub-snippet-uri, trebuie să modificați intrarea pentru snippet-ul părinte pentru a nu genera celelalte sub-snippet-uri. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -În exemplul de mai sus, trebuie să vă asigurați că, pentru o cerere AJAX, doar un singur element va fi adăugat la matricea `$list`, prin urmare, bucla `foreach` va imprima doar un singur fragment dinamic. +Și vom redesena atât fragmentul individual, cât și întreaga zonă de ansamblu: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * This method returns data for the list. - * Usually this would just request the data from a model. - * For the purpose of this example, the data is hard-coded. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +De asemenea, este esențial să ne asigurăm că matricea `$items` conține doar elementele care trebuie redesenate. -Fragmente într-un șablon inclus .[#toc-snippets-in-an-included-template] -======================================================================== - -Se poate întâmpla ca fragmentul să se afle într-un șablon care este inclus dintr-un alt șablon. În acest caz, trebuie să înfășurăm codul de includere în cel de-al doilea șablon cu eticheta `snippetArea`, apoi redesenăm atât snippetArea, cât și fragmentul propriu-zis. - -Eticheta `snippetArea` asigură că codul din interior este executat, dar numai fragmentul real din șablonul inclus este trimis către browser. +Atunci când se inserează un alt șablon în șablonul principal folosind eticheta `{include}`, care are fragmente, este necesar să se înfășoare din nou șablonul inclus într-un `snippetArea` și să se invalideze atât fragmentul, cât și zona împreună: ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* copil.latte *} +{* inclus.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -De asemenea, îl puteți combina cu snippet-uri dinamice. +Snippets în componente .[#toc-snippets-in-components] +----------------------------------------------------- -Adăugarea și ștergerea .[#toc-adding-and-deleting] -================================================== - -Dacă adăugați un nou element în listă și invalidați `itemsContainer`, cererea AJAX returnează fragmente, inclusiv pe cel nou, dar gestionarul javascript nu va putea să îl redea. Acest lucru se datorează faptului că nu există un element HTML cu ID-ul nou creat. - -În acest caz, cea mai simplă metodă este de a îngloba întreaga listă într-un alt fragment și de a-l invalida pe tot: +Puteți crea fragmente în cadrul [componentelor |components], iar Nette le va redesena automat. Cu toate acestea, există o limitare specifică: pentru a redesena snippets, se apelează metoda `render()` fără niciun parametru. Astfel, trecerea parametrilor în șablon nu va funcționa: ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Trimiterea datelor de utilizator .[#toc-sending-user-data] +---------------------------------------------------------- + +Împreună cu fragmente, puteți trimite orice date suplimentare către client. Pur și simplu scrieți-le în obiectul `payload`: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + //... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -Același lucru este valabil și pentru ștergerea unui element. Ar fi posibil să se trimită un fragment gol, dar, de obicei, listele pot fi paginate și ar fi complicat să se implementeze ștergerea unui element și încărcarea altuia (care se afla pe o pagină diferită a listei paginate). +Parametrii de trimitere .[#toc-sending-parameters] +================================================== -Trimiterea parametrilor către componentă .[#toc-sending-parameters-to-component] -================================================================================ - -Atunci când trimitem parametrii către componentă prin intermediul unei cereri AJAX, fie că este vorba de parametri de semnal sau de parametri persistenți, trebuie să furnizăm numele global al acestora, care conține și numele componentei. Numele complet al parametrului returnează metoda `getParameterId()`. +Atunci când trimitem parametrii către componentă prin intermediul unei cereri AJAX, fie că este vorba de parametri de semnal sau de parametri persistenți, trebuie să furnizăm numele lor global, care conține și numele componentei. Numele complet al parametrului returnează metoda `getParameterId()`. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -Și gestionează metoda cu s parametrii corespunzători în componentă. +O metodă handle cu parametrii corespunzători din componentă: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/ro/bootstrap.texy b/application/ro/bootstrap.texy index def23eed0e..26883f892e 100644 --- a/application/ro/bootstrap.texy +++ b/application/ro/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + private Configurator $configurator; + private string $rootDir; + + public function __construct() + { + $this->rootDir = dirname(__DIR__); + // Configuratorul este responsabil de configurarea mediului și a serviciilor aplicației. + $this->configurator = new Configurator; + // Setați directorul pentru fișierele temporare generate de Nette (de exemplu, ș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 { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // Nette este inteligent, iar modul de dezvoltare se activează automat, + // sau îl puteți activa pentru o anumită adresă IP prin decomentarea următoarei linii: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Activează Tracy: instrumentul suprem de depanare "swiss army knife". + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: Încarcă automat toate clasele din directorul dat + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Încarcă fișierele de configurare + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php .[#toc-index-php] =========================== -În cazul aplicațiilor web, fișierul inițial este `index.php`, care se află în directorul public `www/`. Acesta permite clasei `Bootstrap` să inițializeze mediul și să returneze `$configurator` care creează containerul DI. Apoi se obține serviciul `Application`, care execută aplicația web: +În cazul aplicațiilor web, fișierul principal este `index.php`, care este localizat în [directorul public |directory-structure#public-directory-www] `www/`. Astfel, clasa Bootstrap va inițializa mediul și va produce un container DI. Ea obține apoi serviciul `Application` din acesta, care pornește aplicația web: ```php -// inițializarea mediului + obținerea obiectului Configurator -$configurator = App\Bootstrap::boot(); -// creați un container DI -$container = $configurator->createContainer(); -// containerul DI creează un obiect Nette\Application\Application +$bootstrap = new App\Bootstrap; +// Inițializarea mediului + crearea unui container DI +$container = $bootstrap->bootWebApplication(); +// Containerul DI creează un obiect Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -// pornește aplicația Nette +// Porniți aplicația Nette și tratați cererea de intrare $application->run(); ``` @@ -59,26 +84,42 @@ După cum puteți vedea, clasa [api:Nette\Bootstrap\Configurator], pe care o vom Modul de dezvoltare vs. modul de producție .[#toc-development-vs-production-mode] ================================================================================= -Nette face distincție între două moduri de bază în care este executată o cerere: dezvoltare și producție. Modul de dezvoltare este axat pe confortul maxim al programatorului, Tracy este afișat, memoria cache este actualizată automat atunci când se schimbă șabloanele sau configurația containerului DI etc. Modul de producție este axat pe performanță, Tracy înregistrează doar erorile, iar modificările șabloanelor și ale altor fișiere nu sunt verificate. +Nette se comportă diferit în funcție de faptul dacă rulează pe un server de dezvoltare sau de producție: + +🛠️ Modul de dezvoltare: + - Afișează bara de depanare Tracy cu informații utile (de exemplu, interogări SQL, timp de execuție, utilizarea memoriei). + - Afișează o pagină de erori detaliată cu urmele apelurilor funcțiilor și conținutul variabilelor atunci când apare o eroare. + - Actualizează automat memoria cache atunci când sunt modificate șabloanele Latte, fișierele de configurare etc. + + +🚀 Modul de producție: + - Nu afișează nicio informație de depanare; toate erorile sunt înregistrate. + - Afișează o pagină `ErrorPresenter` sau o pagină generică "Server Error" atunci când apare o eroare. + - Cache-ul nu este niciodată reîmprospătat automat! + - Optimizat pentru viteză și securitate. -Selectarea modului se face prin autodetecție, astfel încât, de obicei, nu este nevoie să configurați sau să comutați nimic manual. Modul este dezvoltare dacă aplicația rulează pe localhost (adică adresa IP `127.0.0.1` sau `::1`) și nu este prezent niciun proxy (adică antetul său HTTP). În caz contrar, se execută în modul de producție. + +Modul este determinat automat, astfel încât, în majoritatea cazurilor, nu este nevoie să îl configurați sau să îl comutați manual: + +- Modul de dezvoltare: Activ pe localhost (adresa IP `127.0.0.1` sau `::1`), cu excepția cazului în care se utilizează un proxy (adică, pe baza antetelor sale HTTP). +- Modul de producție: Activ peste tot. Dacă doriți să activați modul de dezvoltare în alte cazuri, de exemplu, pentru programatorii care accesează de la o anumită adresă IP, puteți utiliza `setDebugMode()`: ```php -$configurator->setDebugMode('23.75.345.200'); // una sau mai multe adrese IP +$this->configurator->setDebugMode('23.75.345.200'); // una sau mai multe adrese IP ``` Vă recomandăm cu siguranță să combinați o adresă IP cu un cookie. Vom stoca un token secret în cookie-ul `nette-debug`, de exemplu `secret1234`, iar modul de dezvoltare va fi activat pentru programatorii cu această combinație de IP și cookie. ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` De asemenea, putem dezactiva complet modul de dezvoltare, chiar și pentru localhost: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` Rețineți că valoarea `true` activează modul dezvoltator din greu, ceea ce nu ar trebui să se întâmple niciodată pe un server de producție. @@ -90,7 +131,7 @@ Instrumentul de depanare Tracy .[#toc-debugging-tool-tracy] Pentru o depanare mai ușoară, vom porni minunata unealtă [Tracy |tracy:]. În modul dezvoltator, acesta vizualizează erorile, iar în modul de producție înregistrează erorile în directorul specificat: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ Temporary Files .[#toc-temporary-files] Nette utilizează memoria cache pentru DI container, RobotLoader, șabloane etc. Prin urmare, este necesar să setați calea către directorul în care va fi stocată memoria cache: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` Pe Linux sau macOS, setați [permisiunile de scriere |nette:troubleshooting#Setting directory permissions] pentru directoarele `log/` și `temp/`. @@ -112,7 +153,7 @@ RobotLoader .[#toc-robotloader] De obicei, vom dori să încărcăm automat clasele folosind [RobotLoader |robot-loader:], așa că trebuie să îl pornim și să îl lăsăm să încarce clasele din directorul în care se află `Bootstrap.php` (adică `__DIR__`) și din toate subdirectoarele sale: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ Fusul orar .[#toc-timezone] Configurator vă permite să specificați un fus orar pentru aplicația dumneavoastră. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ Fișierele de configurare sunt de obicei scrise în [formatul NEON |neon:format] Fișierele de configurare sunt încărcate utilizând `addConfig()`: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` Metoda `addConfig()` poate fi apelată de mai multe ori pentru a adăuga mai multe fișiere. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ Parametrii statici .[#toc-static-parameters] Parametrii utilizați în fișierele de configurare pot fi definiți [în secțiunea `parameters` |dependency-injection:configuration#parameters] și, de asemenea, pot fi trecuți (sau suprascriși) prin metoda `addStaticParameters()` (are pseudonimul `addParameters()`). Este important ca valorile diferite ale parametrilor să determine generarea de containere DI suplimentare, adică de clase suplimentare. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ Parametrii dinamici .[#toc-dynamic-parameters] De asemenea, putem adăuga parametri dinamici la container, valorile lor diferite, spre deosebire de parametrii statici, nu vor determina generarea de noi containere DI. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ Variabilele de mediu ar putea fi puse cu ușurință la dispoziție prin intermediul parametrilor dinamici. Le putem accesa prin intermediul `%env.variable%` în fișierele de configurare. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ Puteți utiliza următorii parametri statici în fișierele de configurare: - `%wwwDir%` este calea absolută către directorul care conține fișierul de intrare `index.php` - `%tempDir%` este calea absolută către directorul pentru fișierele temporare - `%vendorDir%` este calea absolută către directorul în care Composer instalează bibliotecile +- `%rootDir%` este calea absolută către directorul rădăcină al proiectului - `%debugMode%` indică dacă aplicația se află în modul de depanare - `%consoleMode%` indică dacă cererea a venit prin linia de comandă @@ -225,7 +268,7 @@ services: Creați o nouă instanță și inserați-o în bootstrap: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Diferite medii .[#toc-different-environments] ============================================= -Nu ezitați să personalizați clasa `Bootstrap` pentru a se potrivi nevoilor dumneavoastră. Puteți adăuga parametri la metoda `boot()` pentru a diferenția proiectele web sau puteți adăuga alte metode, cum ar fi `bootForTests()`, care inițializează mediul pentru testele unitare, `bootForCli()` pentru scripturile apelate din linia de comandă și așa mai departe. +Nu ezitați să personalizați clasa `Bootstrap` în funcție de nevoile dumneavoastră. Puteți adăuga parametri la metoda `bootWebApplication()` pentru a face diferența între proiectele web. Alternativ, puteți adăuga alte metode, cum ar fi `bootTestEnvironment()` pentru a inițializa mediul pentru testele unitare, `bootConsoleApplication()` pentru scripturile apelate din linia de comandă și așa mai departe. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container { - $configurator = self::boot(); Tester\Environment::setup(); // Inițializarea Nette Tester - return $configurator; + $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 index 13ee93eb88..1d48351bbd 100644 --- a/application/ro/components.texy +++ b/application/ro/components.texy @@ -230,6 +230,28 @@ $this->redirect(/* ... */); // și redirecționarea ``` +Redirecționarea după un semnal .[#toc-redirection-after-a-signal] +================================================================= + +După procesarea unui semnal de componentă, urmează adesea redirecționarea. Această situație este similară formularelor - după trimiterea unui formular, redirecționăm, de asemenea, pentru a preveni retrimiterea datelor atunci când pagina este reîmprospătată în browser. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Deoarece o componentă este un element reutilizabil și de obicei nu ar trebui să aibă o dependență directă de anumiți prezentatori, metodele `redirect()` și `link()` interpretează automat parametrul ca fiind un semnal de componentă: + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +Dacă trebuie să redirecționați către un alt prezentator sau acțiune, puteți face acest lucru prin intermediul prezentatorului: + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Parametrii persistenți .[#toc-persistent-parameters] ==================================================== @@ -347,7 +369,7 @@ services: În cele din urmă, vom utiliza această fabrică în prezentatorul nostru: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ Componente în profunzime .[#toc-components-in-depth] Componentele într-o aplicație Nette sunt părțile reutilizabile ale unei aplicații web pe care le încorporăm în pagini, care reprezintă subiectul acestui capitol. Care sunt mai exact capacitățile unei astfel de componente? 1) este redabilă într-un șablon -2) știe ce parte din ea însăși trebuie să redea în timpul unei [cereri AJAX |ajax#invalidation] (fragmente) +2) știe [ce parte din el însuși |ajax#snippets] să redea în timpul unei cereri AJAX (fragmente) 3) are capacitatea de a stoca starea sa într-un URL (parametri persistenți) 4) are capacitatea de a răspunde la acțiunile utilizatorului (semnale) 5) creează o structură ierarhică (în care rădăcina este prezentatorul) diff --git a/application/ro/configuration.texy b/application/ro/configuration.texy index e1ce662525..681c48fcc2 100644 --- a/application/ro/configuration.texy +++ b/application/ro/configuration.texy @@ -13,11 +13,15 @@ application: # afișează panoul "Nette Application" în Tracy BlueScreen? debugger: ... # (bool) valoarea implicită este true - # error-presenter va fi apelat în caz de eroare? - catchExceptions: ... # (bool) implicit la true în modul de producție + # va fi apelat error-presenter la eroare? + # are efect numai în modul dezvoltator + catchExceptions: ... # (bool) implicit la true # numele prezentatorului de erori - errorPresenter: Error # (string) valoarea implicită este "Nette:Error" (Nette:Error) + errorPresenter: Error # (string|array) valoarea implicită este "Nette:Error" (Nette:Error) + + # definește pseudonimele pentru prezentatori și evenimente + aliases: ... # definește regulile de rezolvare a numelui prezentatorului la o clasă mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) valoarea implicită este false ``` -Deoarece error-presenters nu este apelat în mod implicit în modul de dezvoltare, iar erorile sunt afișate de Tracy, schimbarea valorii `catchExceptions` în `true` ajută la verificarea funcționării corecte a error-presenters în timpul dezvoltării. +Începând cu `nette/application` versiunea 3.2, este posibilă definirea unei perechi de prezentatori de erori: + +```neon +application: + errorPresenter: + 4xx: Error4xx # pentru Nette\Application\BadRequestException + 5xx: Error5xx # pentru alte excepții +``` Opțiunea `silentLinks` determină modul în care se comportă Nette în modul de dezvoltare atunci când generarea legăturilor eșuează (de exemplu, pentru că nu există un prezentator etc.). Valoarea implicită `false` înseamnă că Nette declanșează `E_USER_WARNING`. Setarea la `true` suprimă acest mesaj de eroare. Într-un mediu de producție, `E_USER_WARNING` este întotdeauna invocat. De asemenea, putem influența acest comportament prin setarea variabilei presenter [$invalidLinkMode |creating-links#Invalid Links]. - [Cartografierea definește regulile |modules#mapping] prin care numele clasei este derivat din numele prezentatorului. +[Pseudonimele simplifică trimiterea |creating-links#aliases] la prezentatorii utilizați frecvent. + +[Maparea definește regulile |directory-structure#Presenter Mapping] prin care numele clasei este derivat din numele prezentatorului. Înregistrarea automată a prezentatorilor .[#toc-automatic-registration-of-presenters] @@ -82,6 +95,9 @@ latte: # activează [verificarea codului generat |latte:develop#Checking Generated Code] phpLinter: ... # (string) implicit este null + # stabilește localul + locale: cs_CZ # (string) implicit este nul + # clasa lui $this->template templateClass: App\MyTemplateClass # valoarea implicită este Nette\Bridges\ApplicationLatte\DefaultTemplate ``` @@ -91,7 +107,7 @@ Dacă utilizați Latte versiunea 3, puteți adăuga o nouă [extensie |latte:cre ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` /--comment diff --git a/application/ro/creating-links.texy b/application/ro/creating-links.texy index 659cc88a08..b86007453b 100644 --- a/application/ro/creating-links.texy +++ b/application/ro/creating-links.texy @@ -38,7 +38,7 @@ De asemenea, este posibil să se treacă parametri numiți. Următoarea legătur detail ``` -În cazul în care metoda `ProductPresenter::renderShow()` nu are `$lang` în semnătura sa, aceasta poate citi valoarea parametrului folosind `$lang = $this->getParameter('lang')`. +În cazul în care metoda `ProductPresenter::renderShow()` nu are `$lang` în semnătura sa, aceasta poate prelua valoarea parametrului folosind `$lang = $this->getParameter('lang')` sau din [proprietate |presenters#Request Parameters]. În cazul în care parametrii sunt stocați într-o matrice, aceștia pot fi dezvoltați cu operatorul `...` (sau cu operatorul `(expand)` în Latte 2.x): @@ -103,7 +103,7 @@ Dacă acțiunea este `default`, putem omite numele, dar trebuie să păstrăm do home ``` -Legăturile pot indica și alte [module |modules]. Aici, legăturile se disting în relative la submodule sau absolute. Principiul este analog cu cel al căilor de acces pe disc, doar că în loc de bară oblică se folosesc două puncte. Să presupunem că actualul prezentator face parte din modulul `Front`, atunci vom scrie: +De asemenea, legăturile pot trimite la alte [module |directory-structure#Presenters and Templates]. Aici, legăturile se disting în relative la submodule sau absolute. Principiul este analog cu cel al căilor de disc, doar că în loc de bară oblică există două puncte. Să presupunem că prezentatorul real face parte din modulul `Front`, atunci vom scrie: ```latte link to Front:Shop:Product:show @@ -140,7 +140,7 @@ Obiectivul `this` va crea un link către pagina curentă: refresh ``` -În același timp, toți parametrii specificați în semnătura de la `render()` sau `action()` sunt transferați. Astfel, dacă ne aflăm pe paginile `Product:show` și `id:123`, legătura către `this` va transfera și acest parametru. +În același timp, toți parametrii specificați în semnătura `action()` sau `render()` în cazul în care metoda `action()` nu este definită, sunt transferați. Astfel, dacă ne aflăm pe paginile `Product:show` și `id:123`, legătura către `this` va transfera și acest parametru. Desigur, este posibil să se specifice parametrii direct: @@ -213,7 +213,7 @@ Deoarece [componentele |components] sunt unități separate reutilizabile care n Dacă dorim să facem legătura cu prezentatorii din șablonul componentei, folosim eticheta `{plink}`: ```latte -home +home ``` sau în cod @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Aliasuri .[#toc-aliases]{data-version:v3.2.2} +============================================= + +Uneori este util să atribuiți un pseudonim ușor de memorat unei perechi prezentator:acțiune. De exemplu, ați putea numi pagina de pornire `Front:Home:default` simplu `home` sau `Admin:Dashboard:default` `admin` . + +Aliasurile sunt definite în [configurație |configuration] sub cheia `application › aliases`: + +```neon +application: + aliases: + home: Front:Home:default + admin: Admin:Dashboard:default + sign: Front:Sign:in +``` + +În legături, acestea se scriu folosind simbolul at, de exemplu: + +```latte +administration +``` + +Acestea sunt acceptate în toate metodele care lucrează cu legături, cum ar fi `redirect()` și altele similare. + + Legături nevalabile .[#toc-invalid-links] ========================================= @@ -257,6 +281,6 @@ Cum se pot crea legături cu metoda `link()` comfort, dar fără prezența unui LinkGenerator este un serviciu pe care îl puteți avea trecut prin constructor și apoi să creați link-uri folosind metoda sa `link()`. -Există o diferență față de prezentatori. LinkGenerator creează toate legăturile ca URL-uri absolute. În plus, nu există un "prezentator curent", deci nu este posibil să se specifice doar numele acțiunii `link('default')` sau căile relative către [module |modules]. +Comparativ cu prezentatorii, există o diferență. LinkGenerator creează toate legăturile direct ca URL-uri absolute. De asemenea, nu există un "prezentator real", deci nu puteți lista doar numele acțiunii `link('default')` ca țintă sau să listați căi relative către module. Legăturile invalide aruncă întotdeauna `Nette\Application\UI\InvalidLinkException`. diff --git a/application/ro/directory-structure.texy b/application/ro/directory-structure.texy new file mode 100644 index 0000000000..640de61824 --- /dev/null +++ b/application/ro/directory-structure.texy @@ -0,0 +1,526 @@ +Structura de directoare a aplicației +************************************ + +
    + +Cum să proiectați o structură de directoare clară și scalabilă pentru proiectele din Nette Framework? Vă vom arăta practici dovedite care vă vor ajuta să vă organizați codul. Veți învăța: + +- cum să structurați **logic** aplicația în directoare +- cum să proiectați structura astfel încât să **scădească bine** pe măsură ce proiectul crește +- care sunt **alternativele posibile** și avantajele sau dezavantajele acestora + +
    + + +Este important să menționăm că Nette Framework în sine nu insistă asupra unei structuri specifice. Este conceput pentru a fi ușor adaptabil la orice nevoi și preferințe. + + +Structura de bază a proiectului .[#toc-basic-project-structure] +=============================================================== + +Deși Nette Framework nu dictează o structură fixă a directoarelor, există un aranjament implicit dovedit sub forma [Web Project |https://github.com/nette/web-project]: + +/--pre +web-project/ +├── app/ ← directorul aplicației +├── assets/ ← fișiere SCSS, JS, imagini..., alternativ resources/ +├── bin/ ← scripturi de linie de comandă +├── config/ ← configurare +├── log/ ← erori înregistrate +├── temp/ ← fișiere temporare, cache +├── tests/ ← teste +├── vendor/ ← biblioteci instalate de Composer +└── www/ ← directorul public (document-root) +\-- + +Puteți modifica liber această structură în funcție de nevoile dumneavoastră - redenumiți sau mutați foldere. Apoi trebuie doar să ajustați căile relative către directoare în `Bootstrap.php` și eventual `composer.json`. Nu este nevoie de nimic altceva, de nicio reconfigurare complexă, de nicio schimbare constantă. Nette are autodetecție inteligentă și recunoaște automat locația aplicației, inclusiv baza sa URL. + + +Principii de organizare a codului .[#toc-code-organization-principles] +====================================================================== + +Atunci când explorați pentru prima dată un proiect nou, ar trebui să vă puteți orienta rapid. Imaginați-vă că faceți clic pe directorul `app/Model/` și vedeți această structură: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +Din aceasta, veți afla doar că proiectul utilizează anumite servicii, depozite și entități. Nu veți afla nimic despre scopul real al aplicației. + +Să ne uităm la o abordare diferită - **organizarea pe domenii**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Acest lucru este diferit - la prima vedere este clar că acesta este un site de comerț electronic. Numele directoarelor în sine dezvăluie ce poate face aplicația - funcționează cu plăți, comenzi și produse. + +Prima abordare (organizarea după tipul de clasă) aduce mai multe probleme în practică: codul care este legat logic este împrăștiat în diferite foldere și trebuie să săriți între ele. Prin urmare, vom organiza după domenii. + + +Spații de nume .[#toc-namespaces] +--------------------------------- + +Este convențional ca structura directoarelor să corespundă spațiilor de nume din aplicație. Aceasta înseamnă că locația fizică a fișierelor corespunde spațiului de nume al acestora. De exemplu, o clasă localizată în `app/Model/Product/ProductRepository.php` ar trebui să aibă spațiul de nume `App\Model\Product`. Acest principiu ajută la orientarea codului și simplifică încărcarea automată. + + +Singular vs Plural în nume .[#toc-singular-vs-plural-in-names] +-------------------------------------------------------------- + +Observați că folosim singularul pentru directoarele aplicațiilor principale: `app`, `config`, `log`, `temp`, `www`. Același lucru se aplică și în interiorul aplicației: `Model`, `Core`, `Presentation`. Acest lucru se datorează faptului că fiecare reprezintă un concept unificat. + +În mod similar, `app/Model/Product` reprezintă totul despre produse. Nu îl numim `Products` deoarece nu este un folder plin cu produse (care ar conține fișiere precum `iphone.php`, `samsung.php`). Este un spațiu de nume care conține clase pentru lucrul cu produsele - `ProductRepository.php`, `ProductService.php`. + +Dosarul `app/Tasks` este la plural deoarece conține un set de scripturi executabile de sine stătătoare - `CleanupTask.php`, `ImportTask.php`. Fiecare dintre ele este o unitate independentă. + +Din motive de coerență, recomandăm utilizarea: +- Singular pentru spațiile de nume care reprezintă o unitate funcțională (chiar dacă lucrați cu mai multe entități) +- Plural pentru colecții de unități independente +- În caz de incertitudine sau dacă nu doriți să vă gândiți la asta, alegeți singular + + +Director public `www/` .[#toc-public-directory-www] +=================================================== + +Acest director este singurul accesibil de pe web (așa-numitul document-root). Este posibil să întâlniți adesea numele `public/` în loc de `www/` - este doar o chestiune de convenție și nu afectează funcționalitatea. Directorul conține: +- [Punctul de intrare al |bootstrap#index.php] aplicației `index.php` +- `.htaccess` fișier cu reguli mod_rewrite (pentru Apache) +- Fișiere statice (CSS, JavaScript, imagini) +- Fișiere încărcate + +Pentru o securitate adecvată a aplicației, este esențial să aveți [un document-root configurat |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url] corect. + +.[note] +Nu plasați niciodată folderul `node_modules/` în acest director - acesta conține mii de fișiere care pot fi executabile și nu ar trebui să fie accesibile publicului. + + +Director de aplicații `app/` .[#toc-application-directory-app] +============================================================== + +Acesta este directorul principal cu codul aplicației. Structura de bază: + +/--pre +app/ +├── Core/ ← infrastructura contează +├── Model/ ← logică de afaceri +├── Presentation/ ← prezentatoare și șabloane +├── Tasks/ ← scripturi de comandă +└── Bootstrap.php ← clasa bootstrap a aplicației +\-- + +`Bootstrap.php` este [clasa de pornire a aplicației |bootstrap] care inițializează mediul, încarcă configurația și creează containerul DI. + +Să analizăm acum în detaliu subdirectoarele individuale. + + +Prezentatori și șabloane .[#toc-presenters-and-templates] +========================================================= + +Avem partea de prezentare a aplicației în directorul `app/Presentation`. O alternativă este directorul scurt `app/UI`. Acesta este locul pentru toate prezentatoarele, șabloanele lor și orice clase ajutătoare. + +Organizăm acest strat pe domenii. Într-un proiect complex care combină e-commerce, blog și API, structura ar arăta astfel: + +/--pre +app/Presentation/ +├── Shop/ ← e-commerce frontend +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← blog +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administrare +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← puncte finale API + └── V1/ +\-- + +În schimb, pentru un blog simplu am folosi această structură: + +/--pre +app/Presentation/ +├── Front/ ← website frontend +│ ├── Home/ +│ └── Post/ +├── Admin/ ← administrare +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, sitemaps etc. +\-- + +Dosarele precum `Home/` sau `Dashboard/` conțin prezentatori și modele. Dosarele precum `Front/`, `Admin/` sau `Api/` se numesc **module**. Din punct de vedere tehnic, acestea sunt directoare obișnuite care servesc la organizarea logică a aplicației. + +Fiecare folder cu un prezentator conține un prezentator cu nume similar și șabloanele sale. De exemplu, folderul `Dashboard/` conține: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← prezentator +└── default.latte ← șablon +\-- + +Această structură de directoare este reflectată în spațiile de nume ale claselor. De exemplu, `DashboardPresenter` este în spațiul de nume `App\Presentation\Admin\Dashboard` (a se vedea [maparea prezentatorului |#presenter mapping]): + +```php +namespace App\Presentation\Admin\Dashboard; + +class DashboardPresenter extends Nette\Application\UI\Presenter +{ + //... +} +``` + +Ne referim la prezentatorul `Dashboard` din modulul `Admin` al aplicației folosind notația două puncte ca `Admin:Dashboard`. La acțiunea sa `default` atunci ca `Admin:Dashboard:default`. Pentru modulele imbricate, folosim mai multe două puncte, de exemplu `Shop:Order:Detail:default`. + + +Dezvoltarea unei structuri flexibile .[#toc-flexible-structure-development] +--------------------------------------------------------------------------- + +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 de generare a fluxurilor XML. Inițial, avem un formular simplu: + +/--pre +Export/ +├── ExportPresenter.php ← un singur prezentator pentru toate exporturile +├── sitemap.latte ← șablon pentru sitemap +└── feed.latte ← șablon pentru RSS feed +\-- + +Cu timpul, se adaugă mai multe tipuri de feed-uri și avem nevoie de mai multă logică pentru ele... Nicio problemă! Dosarul `Export/` devine pur și simplu un modul: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← feed pentru Amazon + └── ebay.latte ← feed pentru eBay +\-- + +Această transformare este complet lină - trebuie doar să creați noi subfoldere, să împărțiți codul în ele și să actualizați legăturile (de exemplu, de la `Export:feed` la `Export:Feed:amazon`). Datorită acestui fapt, putem extinde treptat structura după cum este necesar, nivelul de anidare nu este limitat în niciun fel. + +De exemplu, dacă în administrare aveți multe prezentatoare legate de gestionarea comenzilor, cum ar fi `OrderDetail`, `OrderEdit`, `OrderDispatch` etc., puteți crea un modul (folder) `Order` pentru o mai bună organizare, care va conține (foldere pentru) prezentatoarele `Detail`, `Edit`, `Dispatch` și altele. + + +Locația șablonului .[#toc-template-location] +-------------------------------------------- + +În exemplele anterioare, am văzut că șabloanele sunt localizate direct în folderul cu prezentatorul: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← prezentator +├── DashboardTemplate.php ← clasă șablon opțională +└── default.latte ← șablon +\-- + +Această locație se dovedește a fi cea mai convenabilă în practică - aveți toate fișierele aferente chiar la îndemână. + +Alternativ, puteți plasa șabloanele într-un subfolder `templates/`. Nette acceptă ambele variante. Puteți chiar plasa șabloanele complet în afara folderului `Presentation/`. Totul despre opțiunile de amplasare a șabloanelor poate fi găsit în capitolul [Căutare șabloane |templates#Template Lookup]. + + +Clase ajutătoare și componente .[#toc-helper-classes-and-components] +-------------------------------------------------------------------- + +Prezentatoarele și șabloanele vin adesea cu alte fișiere ajutătoare. Le plasăm logic în funcție de domeniul lor de aplicare: + +1. **Direct cu prezentatorul** în cazul componentelor specifice pentru prezentatorul dat: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← componentă pentru listarea produselor +└── FilterForm.php ← formular pentru filtrare +\-- + +2. **Pentru modul** - se recomandă utilizarea folderului `Accessory`, care este plasat ordonat 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/`. Iar componentele în `app/Components`. Alegerea depinde de convențiile echipei. + + +Model - inima aplicației .[#toc-model-heart-of-the-application] +=============================================================== + +Modelul conține toată logica de afaceri a aplicației. Pentru organizarea sa, se aplică aceeași regulă - structurăm pe domenii: + +/--pre +app/Model/ +├── Payment/ ← totul despre plăți +│ ├── PaymentFacade.php ← punctul principal de intrare +│ ├── PaymentRepository.php +│ ├── Payment.php ← entitate +├── Order/ ← totul despre comenzi +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← totul despre expediere +\-- + +În model, întâlniți de obicei aceste tipuri de clase: + +**Facade**: reprezintă principalul punct de intrare într-un anumit domeniu al aplicației. Ele acționează ca un orchestrator care coordonează cooperarea între diferite servicii pentru a implementa cazuri de utilizare complete (cum ar fi "crearea comenzii" sau "procesarea plății"). Sub stratul lor de orchestrare, fațada ascunde detaliile de implementare de restul aplicației, oferind astfel o interfață curată pentru lucrul cu domeniul dat. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // validare + // crearea comenzii + // trimiterea de e-mailuri + // scrierea în statistici + } +} +``` + +**Servicii**: se concentrează pe operațiuni comerciale specifice în cadrul unui domeniu. Spre deosebire de facade care orchestrează cazuri de utilizare întregi, un serviciu implementează o logică de afaceri specifică (cum ar fi calcularea prețurilor sau procesarea plăților). Serviciile sunt de obicei fără stare și pot fi utilizate fie de facade ca elemente de bază 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 + { + // calcularea prețului + } +} +``` + +**Repositories**: gestionează toate comunicațiile cu spațiul de stocare a datelor, de obicei o bază de date. Sarcina lor este să încarce și să salveze entități și să implementeze metode de căutare a acestora. Un depozit protejează restul aplicației de detaliile implementării bazei de date și oferă o interfață orientată pe obiect 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 afaceri din aplicație, care au identitatea lor și se modifică în timp. De obicei, acestea sunt clase mapate la tabelele bazei de date utilizând ORM (precum Nette Database Explorer sau Doctrine). Entitățile pot conține reguli de afaceri privind datele lor și logica de validare. + +```php +// Entitate asociată cu tabelul din baza de date comenzi +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. + + +Codul infrastructurii .[#toc-infrastructure-code] +================================================= + +Dosarul `Core/` (sau, de asemenea, `Infrastructure/`) găzduiește baza tehnică a aplicației. Codul de infrastructură include de obicei: + +/--pre +app/Core/ +├── Router/ ← gestionarea rutelor și a URL-urilor +│ └── RouterFactory.php +├── Security/ ← autentificare și autorizare +│ ├── Authenticator.php +│ └── Authorizator.php +├── Logging/ ← logare și monitorizare +│ ├── SentryLogger.php +│ └── FileLogger.php +├── Cache/ ← strat de caching +│ └── FullPageCache.php +└── Integration/ ← integrarea cu servicii ext. + ├── Slack/ + └── Stripe/ +\-- + +Pentru proiectele mai mici, o structură plată este în mod natural suficientă: + +/--pre +Core/ +├── RouterFactory.php +├── Authenticator.php +└── QueueMailer.php +\-- + +Acesta este codul care: + +- Gestionează infrastructura tehnică (rutare, logare, caching) +- integrează servicii externe (Sentry, Elasticsearch, Redis) +- furnizează servicii de bază pentru întreaga aplicație (e-mail, bază de date) +- Este în mare parte independent de domeniul specific - cache-ul sau logger-ul funcționează la fel pentru e-commerce sau blog. + +Vă întrebați dacă o anumită clasă își are locul aici sau în model? Diferența esențială este că codul din `Core/`: + +- Nu știe nimic despre domeniu (produse, comenzi, articole) +- poate fi de obicei transferat într-un alt proiect +- Rezolvă "cum funcționează" (cum să trimiteți e-mail), nu "ce face" (ce e-mail să trimiteți) + +Exemplu pentru o mai bună înțelegere: + +- `App\Core\MailerFactory` - creează instanțe ale clasei de trimitere a e-mailurilor, gestionează setările SMTP +- `App\Model\OrderMailer` - utilizează `MailerFactory` pentru a trimite e-mailuri despre comenzi, cunoaște modelele acestora și când ar trebui trimise + + +Scripturi de comandă .[#toc-command-scripts] +============================================ + +Aplicațiile trebuie adesea să execute sarcini în afara cererilor HTTP obișnuite - fie că este vorba de procesarea datelor în fundal, întreținere sau sarcini periodice. Scripturile simple din directorul `bin/` sunt utilizate pentru execuție, în timp ce logica de implementare efectivă este plasată în `app/Tasks/` (sau `app/Commands/`). + +Exemplu: + +/--pre +app/Tasks/ +├── Maintenance/ ← scripturi de întreținere +│ ├── CleanupCommand.php ← ștergerea datelor vechi +│ └── DbOptimizeCommand.php ← optimizarea bazei de date +├── Integration/ ← integrarea cu sisteme externe +│ ├── ImportProducts.php ← import din sistemul furnizorului +│ └── SyncOrders.php ← sincronizarea comenzilor +└── Scheduled/ ← sarcini regulate + ├── NewsletterCommand.php ← trimiterea de buletine informative + └── ReminderCommand.php ← notificări pentru clienți +\-- + +Ce face parte din model și ce din scripturile de comandă? De exemplu, logica pentru trimiterea unui e-mail face parte din model, trimiterea în masă a mii de e-mailuri face parte din `Tasks/`. + +Sarcinile sunt de obicei executate [din linia de comandă |https://blog.nette.org/en/cli-scripts-in-nette-application] sau prin cron. Ele pot fi, de asemenea, executate prin intermediul unei cereri HTTP, dar trebuie să se țină cont de securitate. Prezentatorul care execută sarcina trebuie să fie securizat, de exemplu numai pentru utilizatorii conectați sau cu un token puternic și acces de la adresele IP permise. Pentru sarcinile lungi, este necesar să creșteți limita de timp a scriptului și să utilizați `session_write_close()` pentru a evita blocarea sesiunii. + + +Alte directoare posibile .[#toc-other-possible-directories] +=========================================================== + +În plus față de directoarele de bază menționate, puteți adăuga alte foldere specializate în funcție de nevoile proiectului. Să ne uităm la cele mai comune și la utilizarea lor: + +/--pre +app/ +├── Api/ ← Logica API independentă de stratul de prezentare +├── Database/ ← scripturi de migrare și seederi pentru datele de testare +├── Components/ ← componente vizuale partajate în întreaga aplicație +├── Event/ ← utile dacă se utilizează o arhitectură bazată pe evenimente +├── Mail/ ← șabloane de e-mail și logica aferentă +└── Utils/ ← clase ajutătoare +\-- + +Pentru componentele vizuale partajate utilizate în prezentatoare în întreaga aplicație, puteți utiliza folderul `app/Components` sau `app/Controls`: + +/--pre +app/Components/ +├── Form/ ← componente partajate pentru formulare +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← componente pentru liste de date +│ └── DataGrid.php +└── Navigation/ ← elemente de navigare + ├── Breadcrumbs.php + └── Menu.php +\-- + +Acesta este locul componentelor cu logică mai complexă. Dacă doriți să partajați componente între mai multe proiecte, este bine să le separați într-un pachet compozitor de sine stătător. + +Î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 +\-- + + +Prezentator Mapping .[#toc-presenter-mapping] +============================================= + +Maparea definește reguli pentru derivarea numelor de clase din numele prezentatorilor. Le specificăm în [configurare |configuration] sub cheia `application › mapping`. + +Pe această pagină, am arătat că plasăm prezentatorii în folderul `app/Presentation` (sau `app/UI`). Trebuie să îi spunem lui Nette despre această convenție în fișierul de configurare. Un singur rând este suficient: + +```neon +application: + mapping: App\Presentation\*\**Presenter +``` + +Cum funcționează maparea? Pentru a înțelege mai bine, să ne imaginăm mai întâi o aplicație fără module. Dorim ca clasele prezentatorului să se încadreze în spațiul de nume `App\Presentation`, astfel încât prezentatorul `Home` să se mapeze la clasa `App\Presentation\HomePresenter`. Acest lucru se realizează cu această configurație: + +```neon +application: + mapping: App\Presentation\*Presenter +``` + +Maparea funcționează prin înlocuirea asteriscului din masca `App\Presentation\*Presenter` cu numele prezentatorului `Home`, rezultând numele final al clasei `App\Presentation\HomePresenter`. Simplu! + +Cu toate acestea, după cum vedeți în exemplele din acest capitol și din alte capitole, plasăm clasele prezentatorului în subdirectoare eponime, de exemplu prezentatorul `Home` se mapează la clasa `App\Presentation\Home\HomePresenter`. Obținem acest lucru prin dublarea două puncte (necesită Nette Application 3.2): + +```neon +application: + mapping: App\Presentation\**Presenter +``` + +Acum vom trece la maparea prezentatorilor în module. Putem defini maparea specifică pentru fiecare modul: + +```neon +application: + mapping: + Front: App\Presentation\Front\**Presenter + Admin: App\Presentation\Admin\**Presenter + Api: App\Api\*Presenter +``` + +Conform acestei configurații, prezentatorul `Front:Home` se mapează la clasa `App\Presentation\Front\Home\HomePresenter`, în timp ce prezentatorul `Api:OAuth` se mapează la clasa `App\Api\OAuthPresenter`. + +Deoarece modulele `Front` și `Admin` au o metodă de corespondență similară și probabil vor exista mai multe astfel de module, este posibil să se creeze o regulă generală care să le înlocuiască. Un nou asterisc pentru modul va fi adăugat la masca clasei: + +```neon +application: + mapping: + *: App\Presentation\*\**Presenter + Api: App\Api\*Presenter +``` + +Aceasta funcționează, de asemenea, pentru structuri de directoare imbricate mai adânc, cum ar fi prezentatorul `Admin:User:Edit`, unde segmentul cu asterisc se repetă pentru fiecare nivel și are ca rezultat clasa `App\Presentation\Admin\User\Edit\EditPresenter`. + +O notație alternativă este utilizarea unui array format din trei segmente în loc de un șir. 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 index 7dacddf162..3e894d7017 100644 --- a/application/ro/how-it-works.texy +++ b/application/ro/how-it-works.texy @@ -22,18 +22,18 @@ Structura directoarelor arată cam așa: /--pre web-project/ ├── app/ ← directory with application -│ ├── Presenters/ ← presenter classes -│ │ ├── HomePresenter.php ← Home presenter class -│ │ └── templates/ ← templates directory -│ │ ├── @layout.latte ← template of shared layout -│ │ └── Home/ ← templates for Home presenter -│ │ └── default.latte ← template for action `default` -│ ├── Router/ ← configuration of URL addresses +│ ├── Core/ ← cursuri de bază necesare +│ │ └── RouterFactory.php ← configurare adrese URL +│ ├── Presentation/ ← prezentări, șabloane & co. +│ │ ├── @layout.latte ← șablon de prezentare partajată +│ │ └── Home/ ← Directorul de prezentatori de acasă +│ │ ├── HomePresenter.php ← Clasa de prezentator de acasă +│ │ └── default.latte ← șablon pentru acțiune default │ └── Bootstrap.php ← booting class Bootstrap ├── bin/ ← scripts for the command line ├── config/ ← configuration files │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← error logs ├── temp/ ← temporary files, cache, … ├── vendor/ ← libraries installed by Composer @@ -45,9 +45,9 @@ Structura directoarelor arată cam așa: └── .htaccess ← prohibits access to all directories except www \-- -Puteți modifica structura directoarelor în orice mod, puteți redenumi sau muta folderele și apoi puteți edita doar căile de acces la `log/` și `temp/` în fișierul `Bootstrap.php` și calea de acces la acest fișier în `composer.json` în secțiunea `autoload`. Nimic mai mult, fără reconfigurări complicate, fără schimbări constante. Nette are o [autodetecție inteligentă |bootstrap#development-vs-production-mode]. +Puteți modifica structura directoarelor după cum doriți, puteți redenumi sau muta foldere - este complet flexibil. Nette dispune, de asemenea, de autodetecție inteligentă și recunoaște automat locația aplicației, inclusiv baza sa URL. -Pentru aplicații ceva mai mari, putem împărți dosarele cu prezentatori și șabloane în subdirectoare (pe disc) și în spații de nume (în cod), pe care le numim [module |modules]. +Pentru aplicații puțin mai mari, putem organiza folderele cu prezentatori și șabloane în [subdirectoare |directory-structure#Presenters and templates] și grupa clasele în spații de nume, pe care le numim module. Directorul `www/` este directorul public sau documentul-rădăcină al proiectului. Îl puteți redenumi fără a fi nevoie să setați nimic altceva pe partea de aplicație. Trebuie doar să [configurați găzduirea |nette:troubleshooting#How to change or remove www directory from URL] astfel încât documentul-root să ajungă în acest director. @@ -75,7 +75,7 @@ Sarcina sa este: Ce fel de fabrică? Noi nu producem tractoare, ci site-uri web! Stați puțin, vă explicăm imediat. -Prin "inițializarea mediului" înțelegem, de exemplu, că este activat [Tracy |tracy:], care este un instrument uimitor pentru înregistrarea sau vizualizarea erorilor. Acesta înregistrează erorile de pe serverul de producție și le afișează direct pe serverul de dezvoltare. Prin urmare, inițializarea trebuie, de asemenea, să decidă dacă site-ul rulează în modul de producție sau de dezvoltare. Pentru a face acest lucru, Nette folosește autodetecția: dacă executați site-ul pe localhost, acesta rulează în modul dezvoltator. Nu trebuie să configurați nimic, iar aplicația este pregătită atât pentru dezvoltare, cât și pentru implementarea în producție. Acești pași sunt realizați și descriși în detaliu în capitolul despre [clasa Bootstrap |bootstrap]. +Prin "inițializarea mediului" înțelegem, de exemplu, activarea [Tracy |tracy:], care este un instrument fantastic pentru înregistrarea și vizualizarea erorilor. Pe serverele de producție, acesta înregistrează erorile, în timp ce pe cele de dezvoltare le afișează direct. Prin urmare, inițializarea include determinarea faptului dacă site-ul rulează în modul de producție sau de dezvoltare. Pentru aceasta, Nette utilizează [autodetecția inteligentă |bootstrap#development-vs-production-mode]: dacă rulați site-ul pe localhost, acesta funcționează în modul de dezvoltare. Nu este necesară nicio configurare, iar aplicația este pregătită atât pentru dezvoltare, cât și pentru implementarea în producție. Aceste etape sunt efectuate și detaliate în capitolul [Bootstrap class |bootstrap]. Al treilea punct (da, l-am sărit pe al doilea, dar vom reveni la el) este să pornim aplicația. Gestionarea cererilor HTTP în Nette este realizată de clasa `Nette\Application\Application` (denumită în continuare `Application`), astfel încât atunci când spunem "a rula o aplicație", ne referim la apelarea unei metode cu numele `run()` pe un obiect din această clasă. @@ -91,7 +91,7 @@ Aplicațiile scrise în Nette sunt împărțite în mai multe așa-numite prezen Aplicația începe prin a cere așa-numitului router să decidă care dintre prezentatori să transmită cererea curentă pentru procesare. Routerul decide a cui este responsabilitatea. Acesta se uită la URL-ul de intrare `https://example.com/product/123`, care dorește să `show` un produs cu `id: 123` ca acțiune. Este un bun obicei să se scrie perechile prezentator + acțiune separate de două puncte ca `Product:show`. -Astfel, routerul a transformat URL-ul într-o pereche `Presenter:action` + parametri, în cazul nostru `Product:show` + `id: 123`. Puteți vedea cum arată un router în fișierul `app/Router/RouterFactory.php` și îl vom descrie în detaliu în capitolul [Routing |Routing]. +Astfel, routerul a transformat URL-ul într-o pereche `Presenter:action` + parametri, în cazul nostru `Product:show` + `id: 123`. Puteți vedea cum arată un router în fișierul `app/Core/RouterFactory.php` și îl vom descrie în detaliu în capitolul [Routing |Routing]. Să mergem mai departe. Aplicația cunoaște deja numele prezentatorului și poate continua. Prin crearea unui obiect `ProductPresenter`, care este codul prezentatorului `Product`. Mai exact, solicită containerului DI crearea prezentatorului, deoarece producerea de obiecte este sarcina acestuia. @@ -121,12 +121,9 @@ Așadar, a fost apelată metoda `renderShow(123)`, al cărei cod este un exemplu Ulterior, prezentatorul returnează răspunsul. Acesta poate fi o pagină HTML, o imagine, un document XML, trimiterea unui fișier de pe disc, JSON sau redirecționarea către o altă pagină. Este important de reținut că, dacă nu spunem în mod explicit cum să răspundem (ceea ce este cazul `ProductPresenter`), răspunsul va fi redarea șablonului cu o pagină HTML. De ce? Ei bine, pentru că în 99% din cazuri dorim să desenăm un șablon, așa că prezentatorul ia acest comportament ca fiind implicit și dorește să ne ușureze munca. Acesta este punctul de vedere al lui Nette. -Nici măcar nu trebuie să precizăm ce șablon să desenăm, el derivă calea către acesta conform unei logici simple. În cazul prezentatorului `Product` și al acțiunii `show`, el încearcă să vadă dacă unul dintre aceste fișiere șablon există în raport cu directorul în care se află clasa `ProductPresenter`: +Nici măcar nu trebuie să specificăm ce șablon să redăm; cadrul va deduce singur calea. În cazul acțiunii `show`, acesta încearcă pur și simplu să încarce șablonul `show.latte` din directorul cu clasa `ProductPresenter`. De asemenea, încearcă să găsească aspectul în fișierul `@layout.latte` (mai multe informații despre [căutarea șabloanelor |templates#Template Lookup]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -De asemenea, va încerca să găsească aspectul în fișierul `@layout.latte` și apoi va reda șablonul. Acum sarcina prezentatorului și a întregii aplicații este finalizată. În cazul în care șablonul nu există, va fi returnată o pagină cu eroarea 404. Puteți citi mai multe despre prezentatori pe pagina [Prezentatori |Presenters]. +Ulterior, șabloanele sunt redate. Acest lucru încheie sarcina prezentatorului și a întregii aplicații, iar activitatea este finalizată. În cazul în care șablonul nu există, ar fi returnată o pagină de eroare 404. Puteți citi mai multe despre prezentatori pe pagina [Prezentatori |presenters]. [* request-flow.svg *] @@ -137,7 +134,7 @@ Doar pentru a fi siguri, să încercăm să recapitulăm întregul proces cu un 3) routerul decodifică URL-ul ca o pereche `Home:default` 4) este creat un obiect `HomePresenter` 5) se apelează metoda `renderDefault()` (dacă există) -6) este redat un șablon `templates/Home/default.latte` cu un layout `templates/@layout.latte` +6) este redat un șablon `default.latte` cu un layout `@layout.latte` Este posibil să fi întâlnit acum o mulțime de concepte noi, dar noi credem că acestea au sens. Crearea de aplicații în Nette este o joacă de copii. diff --git a/application/ro/modules.texy b/application/ro/modules.texy deleted file mode 100644 index bc8e11b0d2..0000000000 --- a/application/ro/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Module -****** - -.[perex] -În Nette, modulele reprezintă unitățile logice care alcătuiesc o aplicație. Acestea includ prezentatori, șabloane, eventual și componente și clase de model. - -Un singur director pentru prezentatori și unul pentru șabloane nu ar fi suficient pentru proiectele reale. A avea zeci de fișiere într-un singur dosar este cel puțin neorganizat. Cum se poate ieși din asta? Pur și simplu le împărțim în subdirectoare pe disc și în spații de nume în cod. Și asta este exact ceea ce fac modulele Nette. - -Așadar, să uităm de un singur dosar pentru prezentatori și șabloane și să creăm în schimb module, de exemplu `Admin` și `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← directory with modules -│ ├── Admin/ ← module Admin -│ │ ├── Presenters/ ← its presenters -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← module Front -│ └── Presenters/ ← its presenters -│ └── ... -\-- - -Această structură de directoare va fi reflectată de spațiile de nume ale claselor, astfel încât, de exemplu, `DashboardPresenter` va fi în spațiul de nume `App\Modules\Admin\Presenters`: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Prezentatorul `Dashboard` din cadrul modulului `Admin` este referit în cadrul aplicației folosind notația de două puncte ca `Admin:Dashboard`, iar acțiunea `default` ca `Admin:Dashboard:default`. -Și de unde știe Nette proper că `Admin:Dashboard` reprezintă clasa `App\Modules\Admin\Presenters\DashboardPresenter`? Acest lucru este determinat de [cartografierea |#mapping] din configurație. -Așadar, structura dată nu este prestabilită și o puteți modifica în funcție de nevoile dumneavoastră. - -Modulele pot conține, bineînțeles, toate celelalte elemente în afară de prezentatori și șabloane, cum ar fi componente, clase de modele etc. - - -Module imbricate .[#toc-nested-modules] ---------------------------------------- - -Modulele nu trebuie să formeze doar o structură plată, ci puteți crea și submodule, de exemplu: - -/--pre -app/ -├── Modules/ ← directory with modules -│ ├── Blog/ ← module Blog -│ │ ├── Admin/ ← submodule Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← submodule Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← module Forum -│ │ └── ... -\-- - -Astfel, modulul `Blog` este împărțit în submodulele `Admin` și `Front`. Din nou, acest lucru se va reflecta în spațiile de nume, care vor fi `App\Modules\Blog\Admin\Presenters` etc. Prezentatorul `Dashboard` din interiorul submodulului este denumit `Blog:Admin:Dashboard`. - -Anveloparea poate merge cât de adânc doriți, astfel încât pot fi create sub-submodule. - - -Crearea de legături .[#toc-creating-links] ------------------------------------------- - -Legăturile din șabloanele de prezentator sunt relative la modulul curent. Astfel, legătura `Foo:default` duce la prezentatorul `Foo` din același modul ca și prezentatorul curent. Dacă modulul curent este `Front`, de exemplu, atunci legătura se prezintă astfel: - -```latte -link to Front:Product:show -``` - -O legătură este relativă chiar dacă include numele unui modul, care este considerat atunci un submodul: - -```latte -link to Front:Shop:Product:show -``` - -Legăturile absolute sunt scrise în mod analog cu căile absolute de acces de pe disc, dar cu două puncte în loc de bară oblică. Astfel, o legătură absolută începe cu două puncte: - -```latte -link to Admin:Product:show -``` - -Pentru a afla dacă ne aflăm într-un anumit modul sau într-un submodul al acestuia, putem utiliza funcția `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Rutarea .[#toc-routing] ------------------------ - -A se vedea [capitolul privind rutarea |routing#Modules]. - - -Cartografiere .[#toc-mapping] ------------------------------ - -Definește regulile prin care numele clasei este derivat din numele prezentatorului. Le scriem în [configurație |configuration] sub cheia `application › mapping`. - -Să începem cu un exemplu care nu folosește module. Vom dori doar ca clasele de prezentator să aibă spațiul de nume `App\Presenters`. Aceasta înseamnă că un prezentator precum `Home` ar trebui să se mapeze la clasa `App\Presenters\HomePresenter`. Acest lucru poate fi realizat prin următoarea configurație: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -Numele prezentatorului este înlocuit cu un asterisc în masca clasei, iar rezultatul este numele clasei. Ușor! - -Dacă împărțim prezentatorii în module, putem avea propria mapare pentru fiecare modul: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Acum, prezentatorul `Front:Home` se referă la clasa `App\Modules\Front\Presenters\HomePresenter` și prezentatorul `Admin:Dashboard` la clasa `App\Modules\Admin\Presenters\DashboardPresenter`. - -Este mai practic să creăm o regulă generală (stea) care să le înlocuiască pe primele două. Asteriscul suplimentar va fi adăugat la masca clasei doar pentru modul: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Dar ce se întâmplă dacă folosim module imbricate și avem un prezentator `Admin:User:Edit`? În acest caz, segmentul cu un asterisc care reprezintă modulul pentru fiecare nivel se repetă pur și simplu, iar rezultatul este clasa `App\Modules\Admin\User\Presenters\EditPresenter`. - -O notație alternativă este utilizarea unui array format din trei segmente în loc de un șir de caractere. Această notație este echivalentă cu cea anterioară: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -Valoarea implicită este `*: *Module\*Presenter`. diff --git a/application/ro/presenters.texy b/application/ro/presenters.texy index c4e64bfad0..56eef37cb0 100644 --- a/application/ro/presenters.texy +++ b/application/ro/presenters.texy @@ -60,7 +60,7 @@ Similar cu metoda `render()`. În timp ce `render()` este destinată Este important ca `action()` este apelat înainte de `render()`, astfel încât în interiorul acesteia să putem eventual să schimbăm următorul curs al ciclului de viață, adică să schimbăm șablonul care va fi redat și, de asemenea, metoda `render()` care va fi apelată, folosind `setView('otherView')`. -Parametrii din cerere sunt trecuți către metodă. Este posibil și recomandat să se precizeze tipurile pentru parametri, de exemplu `actionShow(int $id, string $slug = null)` - dacă parametrul `id` lipsește sau dacă nu este un număr întreg, prezentatorul returnează [eroarea 404 |#Error 404 etc.] și încheie operațiunea. +Parametrii din cerere sunt trecuți către metodă. Este posibil și recomandat să se precizeze tipurile pentru parametri, de exemplu `actionShow(int $id, ?string $slug = null)` - dacă parametrul `id` lipsește sau dacă nu este un număr întreg, prezentatorul returnează [eroarea 404 |#Error 404 etc.] și încheie operațiunea. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ $this->redirect(/* ... */); Eroare 404 etc. .[#toc-error-404-etc] ===================================== -Atunci când nu putem îndeplini cererea deoarece, de exemplu, articolul pe care dorim să îl afișăm nu există în baza de date, vom arunca eroarea 404 folosind metoda `error(string $message = null, int $httpCode = 404)`, care reprezintă eroarea HTTP 404: +Atunci când nu putem îndeplini cererea deoarece, de exemplu, articolul pe care dorim să îl afișăm nu există în baza de date, vom arunca eroarea 404 folosind metoda `error(?string $message = null, int $httpCode = 404)`, care reprezintă eroarea HTTP 404: ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Parametrii cererii .[#toc-request-parameters] +============================================= + +Prezentatorul, ca și fiecare componentă, își obține parametrii din cererea HTTP. Valorile acestora pot fi recuperate cu ajutorul metodei `getParameter($name)` sau `getParameters()`. Valorile sunt șiruri de caractere sau rețele de șiruri de caractere, în esență date brute obținute direct din URL. + +Pentru mai multă comoditate, recomandăm ca parametrii să fie accesibili prin intermediul proprietăților. Este suficient să îi adnotați cu ajutorul caracterului `#[Parameter]` atribut: + +```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 proprietăți, sugerăm să se precizeze tipul de date (de exemplu, `string`). Apoi, Nette va transforma automat valoarea pe baza acestuia. Valorile parametrilor pot fi, de asemenea, [validate |#Validation of Parameters]. + +Atunci când creați o legătură, puteți seta direct valoarea parametrilor: + +```latte +click +``` + + Parametrii persistenți .[#toc-persistent-parameters] ==================================================== @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Dacă `$this->lang` are o valoare, cum ar fi `'en'`, atunci legăturile create folosind `link()` sau `n:href` vor conține și parametrul `lang=en`. Iar atunci când se face clic pe link, acesta va fi din nou `$this->lang = 'en'`. -Pentru proprietăți, vă recomandăm să includeți tipul de date (de exemplu, `string`) și puteți include și o valoare implicită. Valorile parametrilor pot fi [validate |#Validation of Persistent Parameters]. +Pentru proprietăți, vă recomandăm să includeți tipul de date (de exemplu, `string`) și puteți include și o valoare implicită. Valorile parametrilor pot fi [validate |#Validation of Parameters]. Parametrii persistenți sunt trecuți în mod implicit între toate acțiunile unui anumit prezentator. Pentru a-i transmite între mai mulți prezentatori, trebuie să îi definiți fie: @@ -307,18 +333,12 @@ Aprofundare .[#toc-going-deeper] Ceea ce am arătat până acum în acest capitol va fi probabil suficient. Rândurile următoare sunt destinate celor care sunt interesați de prezentatori în profunzime și doresc să știe totul. -Cerință și parametri .[#toc-requirement-and-parameters] +Validarea parametrilor .[#toc-validation-of-parameters] ------------------------------------------------------- -Cererea gestionată de prezentator este obiectul [api:Nette\Application\Request] și este returnată de metoda prezentatorului `getRequest()`. Acesta include o matrice de parametri și fiecare dintre ei aparține fie unora dintre componente, fie direct prezentatorului (care este de fapt tot o componentă, deși una specială). Astfel, Nette redistribuie parametrii și pasează între componentele individuale (și prezentator) prin apelarea metodei `loadState(array $params)`. Parametrii pot fi obținuți prin metoda `getParameters(): array`, folosind individual `getParameter($name)`. Valorile parametrilor sunt șiruri de caractere sau array-uri de șiruri de caractere, acestea fiind practic date brute obținute direct de la un URL. - +Valorile [parametrilor de cerere |#request parameters] și ale [parametrilor persistenți |#persistent parameters] primiți de la URL-uri sunt scrise în proprietăți prin metoda `loadState()`. Aceasta verifică, de asemenea, dacă tipul de date specificat în proprietate corespunde, în caz contrar se va răspunde cu o eroare 404 și pagina nu va fi afișată. -Validarea parametrilor persistenți .[#toc-validation-of-persistent-parameters] ------------------------------------------------------------------------------- - -Valorile [parametrilor persistenți |#persistent parameters] primite de la URL-uri sunt scrise în proprietăți prin metoda `loadState()`. Aceasta verifică, de asemenea, dacă tipul de date specificat în proprietate se potrivește, în caz contrar se va răspunde cu o eroare 404 și pagina nu va fi afișată. - -Nu vă încredeți niciodată orbește în parametrii persistenți, deoarece aceștia pot fi ușor suprascriși de către utilizator în URL. De exemplu, iată cum verificăm dacă `$this->lang` se află printre limbile acceptate. O modalitate bună de a face acest lucru este să suprascrieți metoda `loadState()` menționată mai sus: +Nu vă încredeți niciodată orbește în parametri, deoarece aceștia pot fi ușor suprascriși de către utilizator în URL. De exemplu, iată cum verificăm dacă `$this->lang` se află printre limbile acceptate. O modalitate bună de a face acest lucru este să suprascrieți metoda `loadState()` menționată mai sus: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,7 +361,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Salvarea și restaurarea cererii .[#toc-save-and-restore-the-request] -------------------------------------------------------------------- -Puteți salva cererea curentă într-o sesiune sau o puteți restaura din sesiune și permite prezentatorului să o execute din nou. Acest lucru este util, de exemplu, atunci când un utilizator completează un formular și login-ul său expiră. Pentru a nu pierde date, înainte de redirecționarea către pagina de autentificare, salvăm cererea curentă în sesiune folosind `$reqId = $this->storeRequest()`, care returnează un identificator sub forma unui șir scurt și îl transmite ca parametru către prezentatorul de autentificare. +Cererea pe care o gestionează prezentatorul este un obiect [api:Nette\Application\Request] și este returnată de metoda prezentatorului `getRequest()`. + +Puteți salva cererea curentă într-o sesiune sau o puteți restaura din sesiune și permite prezentatorului să o execute din nou. Acest lucru este util, de exemplu, atunci când un utilizator completează un formular și login-ul său expiră. Pentru a nu pierde date, înainte de redirecționarea către pagina de conectare, salvăm cererea curentă în sesiune cu ajutorul aplicației `$reqId = $this->storeRequest()`, care returnează un identificator sub forma unui șir scurt și îl transmite ca parametru către prezentatorul de conectare. După conectare, apelăm metoda `$this->restoreRequest($reqId)`, care preia cererea din sesiune și o transmite acesteia. Metoda verifică dacă cererea a fost creată de același utilizator ca și cel care s-a logat acum este. Dacă un alt utilizator se conectează sau dacă cheia nu este validă, nu face nimic și programul continuă. @@ -362,7 +384,7 @@ Redirecționarea nu are loc cu o cerere AJAX sau POST, deoarece ar duce la pierd De asemenea, puteți invoca manual canonizarea folosind metoda `canonicalize()`, care, ca și metoda `link()`, primește ca argumente prezentatorul, acțiunile și parametrii. Aceasta creează o legătură și o compară cu URL-ul curent. Dacă este diferit, redirecționează către link-ul generat. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // redirecționează dacă $slug este diferit de $realSlug @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Restricții de acces Utilizarea `#[Requires]` .[#toc-access-restriction-using-requires]{data-version:3.2.2} +---------------------------------------------------------------------------------------------------------- + +Aplicația `#[Requires]` oferă opțiuni avansate pentru restricționarea accesului la prezentatori și la metodele acestora. Acesta poate fi utilizat pentru a specifica metodele HTTP, pentru a solicita cereri AJAX, pentru a limita accesul la aceeași origine și pentru a restricționa accesul doar la redirecționare. Atributul poate fi aplicat atât claselor de prezentatori, cât și metodelor individuale, cum ar fi `action()`, `render()`, `handle()`, și `createComponent()`. + +Puteți specifica aceste restricții: +- pe metodele HTTP: `#[Requires(methods: ['GET', 'POST'])]` +- care necesită o cerere AJAX: `#[Requires(ajax: true)]` +- accesul numai de la aceeași origine: `#[Requires(sameOrigin: true)]` +- acces numai prin redirecționare: `#[Requires(forward: true)]` +- restricții privind anumite acțiuni: `#[Requires(actions: 'default')]` + +Pentru detalii, consultați [Cum se utilizează Requires |best-practices:attribute-requires]. + + +Verificarea metodei HTTP .[#toc-http-method-check] +-------------------------------------------------- + +În Nette, prezentatorii verifică automat metoda HTTP a fiecărei cereri primite, în primul rând din motive de securitate. În mod implicit, sunt permise metodele `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. + +Dacă doriți să activați metode suplimentare, cum ar fi `OPTIONS`, puteți utiliza opțiunea `#[Requires]` attribute (din aplicația 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 efectuează în `checkHttpMethod()`, care verifică dacă metoda specificată în cerere este inclusă în matricea `$presenter->allowedMethods`. Adăugați o metodă de genul următor: + +```php +class MyPresenter extends Nette\Application\UI\Presenter +{ + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } +} +``` + +Este esențial să subliniem faptul că, dacă activați metoda `OPTIONS`, trebuie să o gestionați în mod corespunzător și în prezentator. Această metodă este adesea utilizată ca o așa-numită cerere de preflight, pe care browserele o trimit automat înainte de cererea efectivă atunci când este necesar să se determine dacă cererea este permisă din punctul de vedere al politicii CORS (Cross-Origin Resource Sharing). Dacă permiteți această metodă, dar nu implementați un răspuns adecvat, aceasta poate duce la inconsecvențe și la potențiale probleme de securitate. + + Lecturi suplimentare .[#toc-further-reading] ============================================ diff --git a/application/ro/routing.texy b/application/ro/routing.texy index 4f80ca99be..9689e409bf 100644 --- a/application/ro/routing.texy +++ b/application/ro/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Sau putem folosi această formă, observați rescrierea expresiei regulate de validare: +Pentru o specificație mai detaliată, se poate utiliza o formă și mai extinsă, în care, pe lângă valorile implicite, pot fi stabilite și alte proprietăți ale parametrilor, cum ar fi o expresie regulată de validare (a se vedea parametrul `id` ): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Aceste formate mai vorbărețe sunt utile pentru adăugarea altor metadate. +Este important de reținut faptul că, dacă parametrii definiți în matrice nu sunt incluși în masca de cale, valorile lor nu pot fi modificate, nici măcar folosind parametrii de interogare specificați după un semn de întrebare în URL. Filtre și traduceri .[#toc-filters-and-translations] @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Module .[#toc-modules] ---------------------- -Dacă avem mai multe rute care aparțin unui singur [modul |modules], putem folosi `withModule()` pentru a le grupa: +Dacă avem mai multe rute care aparțin unui singur [modul |directory-structure#Presenters and Templates], putem utiliza `withModule()` pentru a le grupa: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Integrare .[#toc-integration] ============================= -Pentru a conecta routerul nostru la aplicație, trebuie să informăm containerul DI despre acesta. Cel mai simplu este să pregătim fabrica care va construi obiectul router și să spunem configurației containerului să o folosească. Să spunem că scriem o metodă în acest scop `App\Router\RouterFactory::createRouter()`: +Pentru a conecta routerul nostru la aplicație, trebuie să informăm containerul DI despre acesta. Cel mai simplu este să pregătim fabrica care va construi obiectul router și să spunem configurației containerului să o folosească. Să spunem că scriem o metodă în acest scop `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ Apoi scriem în [configurație |dependency-injection:services]: ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Orice dependență, cum ar fi o conexiune la o bază de date etc., este transmisă metodei factory ca parametru al acesteia folosind [autowiring |dependency-injection:autowiring]: @@ -663,7 +663,7 @@ Prin utilizare separată se înțelege utilizarea capacităților routerului în Deci, din nou, vom crea o metodă care va construi un router, de exemplu: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Sau vom crea obiecte direct: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/ro/templates.texy b/application/ro/templates.texy index abcead647e..170a984b50 100644 --- a/application/ro/templates.texy +++ b/application/ro/templates.texy @@ -34,35 +34,81 @@ Iar acesta ar putea fi șablonul de acțiune: Acesta definește blocul `content`, care este inserat în locul lui `{include content}` în machetă și, de asemenea, redefinește blocul `title`, care suprascrie `{block title}` în machetă. Încercați să vă imaginați rezultatul. -Căutați șabloane .[#toc-search-for-templates] ---------------------------------------------- +Căutarea șablonului .[#toc-template-lookup] +------------------------------------------- + +În prezentări, nu trebuie să specificați ce șablon trebuie redat; cadrul va determina automat calea, ceea ce vă ușurează codificarea. + +Dacă utilizați o structură de directoare în care fiecare prezentator are propriul director, plasați pur și simplu șablonul în acest director sub numele acțiunii (de exemplu, view). De exemplu, pentru acțiunea `default`, utilizați șablonul `default.latte`: -Calea către șabloane este dedusă conform unei logici simple. Se încearcă să se vadă dacă unul dintre aceste fișiere șablon există în raport cu directorul în care se află clasa presenter, unde `` este numele prezentatorului curent și `` este numele acțiunii curente: +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -- `templates//.latte` -- `templates/..latte` +Dacă utilizați o structură în care prezentatorii sunt împreună într-un singur director, iar șabloanele într-un dosar `templates`, salvați-l fie într-un fișier `..latte` sau `/.latte`: -Dacă șablonul nu este găsit, se va încerca căutarea în directorul `templates` cu un nivel mai sus, adică la același nivel cu directorul cu clasa de prezentator. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- -Dacă șablonul nu este găsit nici acolo, răspunsul este o [eroare 404 |presenters#Error 404 etc.]. +Directorul `templates` poate fi, de asemenea, plasat un nivel mai sus, la același nivel cu directorul cu clasele de prezentatori. -De asemenea, puteți schimba vizualizarea folosind `$this->setView('otherView')`. Sau, în loc să căutați, specificați direct numele fișierului șablon folosind `$this->template->setFile('/path/to/template.latte')`. +În cazul în care șablonul nu este găsit, prezentatorul răspunde cu [eroarea 404 - page not found |presenters#Error 404 etc]. + +Puteți schimba vizualizarea folosind `$this->setView('anotherView')`. De asemenea, este posibilă specificarea directă a fișierului șablon cu `$this->template->setFile('/path/to/template.latte')`. .[note] -Puteți modifica căile în care sunt căutate șabloanele prin suprascrierea metodei [formatTemplateFiles |api:Nette\Application\UI\Presenter::formatTemplateFiles()], care returnează o matrice de posibile căi de acces la fișiere. +Fișierele în care sunt căutate șabloanele pot fi modificate prin suprascrierea metodei [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], care returnează o matrice de nume de fișiere posibile. + + +Căutarea șabloanelor de prezentare .[#toc-layout-template-lookup] +----------------------------------------------------------------- + +De asemenea, Nette caută automat fișierul de machetare. + +Dacă folosiți o structură de directoare în care fiecare prezentator are propriul director, plasați macheta fie în folderul cu prezentatorul, dacă este specifică doar acestuia, fie la un nivel superior, dacă este comună mai multor prezentatori: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +Dacă utilizați o structură în care prezentatorii sunt grupați într-un singur director, iar șabloanele se află într-un dosar `templates`, macheta va fi așteptată în următoarele locuri: -Prezentarea este așteptată în următoarele fișiere: +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` aspect comun pentru mai mulți prezentatori +Dacă prezentatorul se află într-un modul, acesta va căuta și mai sus în arborele directoarelor, în conformitate cu cuiburile modulului. -`` este numele prezentatorului curent și `` este numele structurii, care este în mod implicit `'layout'`. Numele poate fi schimbat cu `$this->setLayout('otherLayout')`, astfel încât să se încerce să se utilizeze fișierele `@otherLayout.latte`. +Numele prezentării poate fi schimbat folosind `$this->setLayout('layoutAdmin')` și apoi va fi așteptat în fișierul `@layoutAdmin.latte`. De asemenea, puteți specifica direct fișierul șablon de prezentare folosind `$this->setLayout('/path/to/template.latte')`. -De asemenea, puteți specifica direct numele de fișier al șablonului de machetă cu `$this->setLayout('/path/to/template.latte')`. Utilizarea `$this->setLayout(false)` va dezactiva căutarea de machete. +Utilizarea `$this->setLayout(false)` sau a etichetei `{layout none}` în interiorul șablonului dezactivează căutarea de layout. .[note] -Puteți modifica căile de căutare a șabloanelor prin suprascrierea metodei [formatLayoutTemplateFiles |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], care returnează o matrice de posibile căi de acces la fișiere. +Fișierele în care sunt căutate șabloanele de prezentare pot fi modificate prin suprascrierea metodei [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], care returnează o matrice de nume de fișiere posibile. Variabilele din șablon .[#toc-variables-in-the-template] @@ -104,7 +150,7 @@ Adnotarea `@property-read` este pentru IDE și analiza statică, va face ca auto Vă puteți permite și luxul de a șopti în șabloane, trebuie doar să instalați pluginul în PhpStorm și să specificați numele clasei la începutul șablonului, consultați articolul "Latte: cum se tastează sistemul":https://blog.nette.org/ro/latte-cum-se-utilizeaza-sistemul-de-tipuri: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void Latte versiunea 3 oferă o metodă mai avansată prin crearea unei [extensii |latte:creating-extension] pentru fiecare proiect web. Iată un exemplu aproximativ al unei astfel de clase: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ O înregistrăm folosind [configuration#Latte]: ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ Alternativ, traducătorul poate fi setat cu ajutorul [configurației |configurat ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` Traducătorul poate fi apoi utilizat, de exemplu, ca filtru `|translate`, cu parametri suplimentari trecuți la metoda `translate()` (a se vedea `foo, bar`): diff --git a/application/ru/@home.texy b/application/ru/@home.texy index 6ba4943c53..e8073aad30 100644 --- a/application/ru/@home.texy +++ b/application/ru/@home.texy @@ -2,35 +2,84 @@ **************** .[perex] -Пакет `nette/application` является основой для создания интерактивных веб-приложений. +Nette Application - это ядро фреймворка Nette, который предоставляет мощные инструменты для создания современных веб-приложений. Он предлагает множество исключительных возможностей, которые значительно упрощают разработку и повышают безопасность и сопровождаемость кода. -- [Как работают приложения? |how-it-works] -- [Bootstrap] -- [Презентеры |presenters] -- [Шаблоны |templates] -- [Модули |modules] -- [Маршрутизация |routing] -- [Создание URL-ссылок |creating-links] -- [Интерактивные компоненты |components] -- [AJAX и сниппеты |ajax] -- [Multiplier] -- [Конфигурация |configuration] +Установка .[#toc-installation] +------------------------------ -Установка ---------- - -Загрузите и установите пакет с помощью [Composer|best-practices:composer]: +Загрузите и установите библиотеку с помощью [Composer |best-practices:composer]: ```shell composer require nette/application ``` -| версия пакета | совместимая версия PHP -|-----------------------|----------------------- -| Nette Application 4.0 | PHP 8.0 – 8.1 -| Nette Application 3.1 | PHP 7.2 – 8.1 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 -Применяется к последним версиям патчей. +Почему стоит выбрать Nette Application? .[#toc-why-choose-nette-application] +---------------------------------------------------------------------------- + +Компания Nette всегда была пионером в области веб-технологий. + +**Двунаправленный маршрутизатор:** В Nette реализована передовая система маршрутизации, уникальная своей двунаправленностью - она не только транслирует URL в действия приложения, но и может генерировать URL в обратном направлении. Это означает: +- Вы можете в любой момент изменить структуру URL всего приложения, не изменяя файлы шаблонов. +- URL автоматически канонизируются, что улучшает SEO +- Маршрутизация задается в одном месте, а не разбросана по аннотациям + +**Компоненты и сигналы:** Встроенная система компонентов, вдохновленная Delphi и React.js, является уникальной среди PHP-фреймворков: +- Позволяет создавать многократно используемые элементы пользовательского интерфейса +- Поддерживает иерархическую композицию компонентов +- Предлагает элегантную обработку 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 с системой типов + +**Инъекция зависимостей:** Nette полностью использует инъекцию зависимостей: +- Автоматическая передача зависимостей (autowiring) +- Конфигурирование с использованием понятного формата NEON +- Поддержка фабрик компонентов + + +Основные преимущества .[#toc-main-benefits] +------------------------------------------- + +- **Безопасность**: Автоматическая защита от таких [уязвимостей |nette:vulnerability-protection], как XSS, CSRF и т. д. +- **Производительность**: Меньше писанины, больше возможностей благодаря продуманному дизайну +- **Отладка**: [Отладчик |tracy:] с панелью маршрутизации +- **Производительность**: Интеллектуальная система кэширования, ленивая загрузка компонентов +- **Гибкость**: Легкая модификация URL даже после завершения работы приложения +- **Компоненты**: Уникальная система многократно используемых элементов пользовательского интерфейса +- **Современный**: Полная поддержка PHP 8.4+ и системы типов + + +Начало работы .[#toc-getting-started] +------------------------------------- + +1. [Понимание приложений |how-it-works] - Понимание базовой архитектуры +2. [Презентаторы |presenters] - Работа с презентаторами и действиями +3. [Шаблоны |templates] - Создание шаблонов в Latte +4. [Маршрутизация |routing] - настройка URL +5. [Интерактивные компоненты |components] - использование системы компонентов + + +Совместимость с PHP .[#toc-php-compatibility] +--------------------------------------------- + +| Версия | совместимая с 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 + +Действителен для последних версий патчей. \ No newline at end of file diff --git a/application/ru/@left-menu.texy b/application/ru/@left-menu.texy index a0ba69fe6b..3ff868ece5 100644 --- a/application/ru/@left-menu.texy +++ b/application/ru/@left-menu.texy @@ -4,7 +4,7 @@ - [Bootstrap] - [Презентеры |presenters] - [Шаблоны |templates] -- [Модули |modules] +- [Структура каталога |directory-structure] - [Маршрутизация |routing] - [Создание URL-ссылок |creating-links] - [Интерактивные компоненты |components] diff --git a/application/ru/ajax.texy b/application/ru/ajax.texy index 6009fc864c..ffcb026607 100644 --- a/application/ru/ajax.texy +++ b/application/ru/ajax.texy @@ -3,10 +3,10 @@ AJAX и сниппеты
    -Современные веб-приложения сегодня работают наполовину на сервере, а наполовину в браузере. AJAX является жизненно важным объединяющим фактором. Какую поддержку предлагает фреймворк Nette? -- отправка фрагментов шаблонов (так называемых *сниппетов*) +В эпоху современных веб-приложений, когда функциональность часто разрывается между сервером и браузером, AJAX является необходимым связующим элементом. Какие возможности предлагает Nette Framework в этой области? +- передача частей шаблона, так называемых сниппетов - передача переменных между PHP и JavaScript -- Отладка приложений AJAX +- инструменты для отладки AJAX-запросов
    @@ -14,29 +14,32 @@ AJAX и сниппеты Запрос AJAX .[#toc-ajax-request] ================================ -AJAX-запрос не отличается от классического запроса - к ведущему обращаются с определенным представлением и параметрами. Ведущий также решает, как ответить на него: он может использовать свою собственную процедуру, которая возвращает фрагмент HTML-кода (HTML snippet), XML-документ, JSON-объект или JavaScript-код. +AJAX-запрос принципиально не отличается от классического HTTP-запроса. Вызывается презентатор с определенными параметрами. Как ответить на запрос, зависит от ведущего - он может вернуть данные в формате JSON, отправить часть HTML-кода, XML-документ и т.д. -На стороне сервера AJAX-запрос может быть обнаружен с помощью сервисного метода, [инкапсулирующего HTTP-запрос |http:request] `$httpRequest->isAjax()` (определяет на основе HTTP-заголовка `X-Requested-With`). Внутри презентатора доступен ярлык в виде метода `$this->isAjax()`. +На стороне браузера мы инициируем AJAX-запрос с помощью функции `fetch()`: -Существует предварительно обработанный объект `payload`, предназначенный для отправки данных в браузер в формате JSON. - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Успешно'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // обработка ответа +}); ``` -Для полного контроля над выводом JSON используйте метод `sendJson` в презентере. Это немедленно прервет работу презентера, и вы обойдетесь без шаблона: +На стороне сервера AJAX-запрос распознается методом `$httpRequest->isAjax()` сервиса, [инкапсулирующего HTTP-запрос |http:request]. Он использует HTTP-заголовок `X-Requested-With`, поэтому его необходимо отправить. Внутри презентатора можно использовать метод `$this->isAjax()`. + +Если необходимо отправить данные в формате JSON, используйте метод [`sendJson()` |presenters#Sending a response] метод. Метод также завершает работу презентера. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Если мы хотим отправить HTML, мы можем установить специальный шаблон для AJAX-запросов: +Если вы планируете отвечать с помощью специального шаблона, предназначенного для AJAX, то это можно сделать следующим образом: ```php public function handleClick($param): void @@ -44,27 +47,43 @@ public function handleClick($param): void if ($this->isAjax()) { $this->template->setFile('path/to/ajax.latte'); } - // ... + //... } ``` -Naja -==== +Фрагменты .[#toc-snippets] +========================== + +Наиболее мощным инструментом, предлагаемым Nette для связи сервера с клиентом, являются сниппеты. С их помощью можно превратить обычное приложение в AJAX-приложение, затратив минимум усилий и написав несколько строк кода. Пример Fifteen демонстрирует, как это работает, а его код можно найти на [GitHub |https://github.com/nette-examples/fifteen]. + +Сниппеты, или вырезки, позволяют обновлять только отдельные части страницы, а не перезагружать ее целиком. Это быстрее и эффективнее, а также обеспечивает более комфортную работу пользователя. Возможно, сниппеты напомнят вам Hotwire для Ruby on Rails или Symfony UX Turbo. Интересно, что компания Nette представила сниппеты на 14 лет раньше. -[Библиотека Naja |https://naja.js.org] используется для обработки AJAX-запросов на стороне браузера. [Установите |https://naja.js.org/#/guide/01-install-setup-naja] его как пакет node.js (для использования с Webpack, Rollup, Vite, Parcel и другими): +Как работают сниппеты? При первой загрузке страницы (не-AJAX-запрос) загружается вся страница, включая все сниппеты. Когда пользователь взаимодействует со страницей (например, нажимает кнопку, отправляет форму и т.д.), вместо загрузки всей страницы выполняется AJAX-запрос. Код в презентере выполняет это действие и решает, какие фрагменты необходимо обновить. Nette рендерит эти фрагменты и отправляет их в виде массива JSON. Затем код обработки в браузере вставляет полученные фрагменты обратно на страницу. Таким образом, передается только код измененных фрагментов, что позволяет экономить пропускную способность и ускорять загрузку по сравнению с передачей всего содержимого страницы. + + +Naja .[#toc-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 ``` -...или вставить непосредственно в шаблон страницы: +... или вставьте ее непосредственно в шаблон страницы: ```html ``` -Чтобы создать AJAX-запрос из обычной ссылки (сигнала) или отправки формы, просто пометьте соответствующую ссылку, форму или кнопку классом `ajax`: +Сначала нужно [инициализировать |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] библиотеку: + +```js +naja.initialize(); +``` + +Чтобы превратить обычную ссылку (сигнал) или отправку формы в AJAX-запрос, достаточно пометить соответствующую ссылку, форму или кнопку классом `ajax`: ```html Go @@ -74,64 +93,39 @@ npm install naja or +
    ``` -Сниппеты -======== - -Однако существует гораздо более мощный инструмент встроенной поддержки AJAX — сниппеты. Их использование позволяет превратить обычное приложение в AJAX-приложение с помощью всего нескольких строк кода. Как всё это работает, показано в примере Fifteen, код которого также доступен в сборке или на [GitHub |https://github.com/nette-examples/fifteen]. - -Принцип работы сниппетов заключается в том, что вся страница передается во время начального (т. е. не-AJAX) запроса и затем с каждым AJAX [subrequest |components#Signal] (запрос того же представления того же презентера) только код измененных частей передается в хранилище `payload`, упомянутое ранее. - -Сниппеты могут напомнить вам Hotwire для Ruby on Rails или Symfony UX Turbo, но Nette придумал их четырнадцатью годами раньше. - - -Инвалидация .[#toc-invalidation-of-snippets] -============================================ +Перерисовка фрагментов .[#toc-redrawing-snippets] +------------------------------------------------- -Каждый потомок класса [Control |components] (которым является и Presenter) способен помнить, были ли какие-либо изменения во время запроса, требующие повторного отображения. Существует несколько способов справиться с этим: `redrawControl()` и `isControlInvalid()`. Пример: +Каждый объект класса [Control |components] (в том числе и сам Presenter) хранит информацию о том, произошли ли изменения, требующие его перерисовки. Для этого используется метод `redrawControl()`. ```php public function handleLogin(string $user): void { - // Объект должен повторно отображаться после того, как пользователь вошел в систему + // после входа в систему необходимо перерисовать соответствующую часть $this->redrawControl(); - // ... + //... } ``` -Однако Nette обеспечивает ещё более тонкое разрешение, чем целые компоненты. Перечисленные методы принимают имя так называемого «фрагмента» в качестве необязательного параметра. «Фрагмет» это, по сути, элемент в вашем шаблоне, помеченный для этой цели макросом Latte, подробнее об этом позже. Таким образом, можно попросить компонент перерисовать только *часть* своего шаблона. Если весь компонент недействителен, то все его фрагменты отображаются заново. Компонент является «недействительным», если любой из его субкомпонентов является недействительным. -```php -$this->isControlInvalid(); // -> false - -$this->redrawControl('header'); // аннулирует фрагмент с именем 'header' -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, по крайней мере один фрагмент недействителен +Nette также позволяет более тонко контролировать, что именно нужно перерисовывать. Упомянутый выше метод может принимать в качестве аргумента имя фрагмента. Таким образом, можно аннулировать (то есть принудительно перерисовать) на уровне части шаблона. Если аннулируется весь компонент, то перерисовывается и каждый его фрагмент: -$this->redrawControl(); // делает недействительным весь компонент, каждый фрагмент -$this->isControlInvalid('footer'); // -> true +```php +// аннулирует фрагмент 'header' +$this->redrawControl('header'); ``` -Компонент, получивший сигнал, автоматически помечается для перерисовки. - -Благодаря перерисовке фрагментов мы точно знаем, какие части каких элементов должны быть перерисованы. - - -Тег `{snippet} … {/snippet}` .{toc: Tag snippet} -================================================ - -Рендеринг страницы происходит точно так же, как и при обычном запросе: загружаются одни и те же шаблоны и т. д. Однако самое важное — это не допустить попадания в выходной сигнал тех частей, которые не должны попасть в выходной сигнал; остальные части должны быть связаны с идентификатором и отправлены пользователю в формате, понятном для обработчика JavaScript. - -Синтаксис ---------- +Фрагменты в Latte .[#toc-snippets-in-latte] +------------------------------------------- -Если в шаблоне есть элемент управления или фрагмент, мы должны обернуть его с помощью парного тега `{snippet} ... {/snippet}` — отрисованный фрагмент будет «вырезан» и отправится в браузер. Он также заключит его в вспомогательный тег `
    ` (можно использовать другой). В следующем примере определен сниппет с именем `header`. Он также может представлять собой шаблон компонента: +Использовать сниппеты в Latte очень просто. Чтобы определить часть шаблона как сниппет, достаточно обернуть ее в теги `{snippet}` и `{/snippet}`: ```latte {snippet header} @@ -139,7 +133,9 @@ $this->isControlInvalid('footer'); // -> true {/snippet} ``` -Если вы хотите создать сниппет с другим содержащим элементом, отличным от `
    `, или добавить пользовательские атрибуты к элементу, вы можете использовать следующее определение: +Сниппет создает элемент `
    ` в HTML-странице со специально сгенерированным `id`. При перерисовке сниппета содержимое этого элемента обновляется. Поэтому при первоначальном рендеринге страницы все сниппеты также должны быть рендерированы, даже если они изначально могут быть пустыми. + +Также можно создать сниппет с элементом, отличным от `
    ` с помощью атрибута n:attribute: ```latte
    @@ -148,138 +144,106 @@ $this->isControlInvalid('footer'); // -> true ``` -Динамические сниппеты -===================== +Области фрагментов .[#toc-snippet-areas] +---------------------------------------- -В Nette вы также можете определить сниппеты с динамическим именем, основанным на параметре времени выполнения. Это наиболее подходит для различных списков, где нам нужно изменить только одну строку, но мы не хотим переносить весь список вместе с ней. Примером этого может быть: +Имена сниппетов также могут быть выражениями: ```latte - +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Существует один статический сниппет `itemsContainer`, содержащий несколько динамических сниппетов: `пункт-0`, `пункт-1` и так далее. +Таким образом, мы получим несколько фрагментов типа `item-0`, `item-1`, и т.д. Если мы напрямую аннулируем динамический сниппет (например, `item-1`), то ничего не будет перерисовано. Причина в том, что сниппеты работают как настоящие выдержки, и непосредственно отрисовываются только они сами. Однако в шаблоне технически нет фрагмента с именем `item-1`. Он появляется только при выполнении окружающего сниппета кода, в данном случае цикла foreach. Поэтому часть шаблона, которую необходимо выполнить, мы пометим тегом `{snippetArea}`: -Вы не можете перерисовать динамический фрагмент напрямую (перерисовка `item-1` не имеет эффекта), вы должны перерисовать его родительский фрагмент (в данном примере `itemsContainer`). При этом выполняется код родительского сниппета, но браузеру передаются только его вложенные сниппеты. Если вы хотите передать только один из вложенных сниппетов, вам нужно изменить ввод для родительского сниппета, чтобы не генерировать другие вложенные сниппеты. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -В приведенном примере необходимо убедиться, что при AJAX-запросе в массив `$list` будет добавлен только один элемент, поэтому цикл `foreach` будет выводить только один динамический фрагмент. +И перерисуем как отдельный фрагмент, так и всю общую область: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * Этот метод возвращает данные для списка. - * Обычно это просто запрос данных из модели. - * Для целей этого примера данные жёстко закодированы. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +Также необходимо убедиться, что массив `$items` содержит только те элементы, которые должны быть перерисованы. -Сниппеты во включенном шаблоне -============================== - -Может случиться так, что сниппет находится в шаблоне, который включается из другого шаблона. В этом случае необходимо обернуть код включения во втором шаблоне макросом `snippetArea`, затем перерисовать как snippetArea, так и сам сниппет. - -Макрос `snippetArea` гарантирует, что код внутри него будет выполнен, но браузеру будет отправлен только фактический фрагмент включенного шаблона. +При вставке другого шаблона в основной с помощью тега `{include}`, содержащего сниппеты, необходимо снова обернуть включаемый шаблон в `snippetArea` и сделать недействительными и сниппет, и область вместе: ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* included.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -Вы также можете сочетать его с динамическими сниппетами. +Сниппеты в компонентах .[#toc-snippets-in-components] +----------------------------------------------------- -Добавление и удаление -===================== - -Если добавить новый элемент в список и аннулировать `itemsContainer`, AJAX-запрос вернет фрагменты, включая новый, но javascript-обработчик не сможет его отобразить. Это происходит потому, что нет HTML-элемента с вновь созданным ID. - -В этом случае самый простой способ — обернуть весь список в ещё один сниппет и признать его недействительным: +Вы можете создавать сниппеты внутри [компонентов |components], и Nette будет автоматически перерисовывать их. Однако есть одно ограничение: для перерисовки сниппетов вызывается метод `render()` без параметров. Таким образом, передача параметров в шаблоне не работает: ```latte -{snippet wholeList} - -{/snippet} -Добавить +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Отправка пользовательских данных .[#toc-sending-user-data] +---------------------------------------------------------- + +Наряду со сниппетами можно отправлять клиенту любые дополнительные данные. Для этого достаточно записать их в объект `payload`: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + //... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -То же самое относится и к удалению элемента. Можно было бы отправить пустой сниппет, но обычно списки могут быть постраничными, и было бы сложно реализовать удаление одного элемента и загрузку другого (который раньше находился на другой странице постраничного списка). - -Отправка параметров компоненту -============================== +Параметры отправки .[#toc-sending-parameters] +============================================= -Когда мы отправляем параметры компоненту через AJAX-запрос, будь то сигнальные или постоянные параметры, мы должны предоставить их глобальное имя, которое также содержит имя компонента. Полное имя параметра возвращает метод `getParameterId()`. +Когда мы отправляем в компонент через AJAX-запрос параметры, будь то сигнальные или постоянные, мы должны указать их глобальное имя, которое также содержит имя компонента. Полное имя параметра возвращает метод `getParameterId()`. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -И обработать метод с соответствующими параметрами в компоненте. +handle метод с соответствующими параметрами в компоненте: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/ru/bootstrap.texy b/application/ru/bootstrap.texy index 071d2c9fc5..b94156d7bd 100644 --- a/application/ru/bootstrap.texy +++ b/application/ru/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + 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 { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // Nette умный, и режим разработки включается автоматически, + // или вы можете включить его для определенного IP-адреса, откомментировав следующую строку: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Включает Tracy: основной инструмент отладки в виде "армейского ножа". + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: автозагрузка всех классов в заданной директории + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Загрузка конфигурационных файлов + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php ========= -В случае веб-приложений начальным файлом является `index.php`, который находится в общедоступном каталоге `www/`. Он позволяет классу `Bootstrap` инициализировать среду и возвращает `$configurator`, который создает контейнер DI. Затем он получает сервис `Application`, запускающий веб-приложение: +В случае с веб-приложениями основным файлом является `index.php`, который находится в [публичной директории |directory-structure#public-directory-www] `www/`. В нем класс Bootstrap инициализирует окружение и создает DI-контейнер. Затем он получает из него службу `Application`, которая запускает веб-приложение: ```php -// инициализируем среду + получаем объект Configurator -$configurator = App\Bootstrap::boot(); -// создаем DI-контейнер -$container = $configurator->createContainer(); -// DI-контейнер создайет объект Nette\Application\Application +$bootstrap = new App\Bootstrap; +// Инициализация среды + создание DI-контейнера +$container = $bootstrap->bootWebApplication(); +// DI-контейнер создает объект Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -// запускаем приложение Nette +// Запуск приложения Nette и обработка входящего запроса $application->run(); ``` @@ -59,26 +84,42 @@ $application->run(); Режим разработки и режим производства .[#toc-development-vs-production-mode] ============================================================================ -Nette различает два основных режима, в которых выполняется запрос: разработка и производство. Режим разработки ориентирован на максимальное удобство программиста, отображается Tracy, кэш автоматически обновляется при изменении шаблонов или конфигурации DI контейнера и т. д. Режим производства ориентирован на производительность, Tracy только регистрирует ошибки, а изменения шаблонов и других файлов не проверяются. +Nette ведет себя по-разному в зависимости от того, работает ли он на сервере разработки или на рабочем сервере: + +🛠️ Режим разработки: + - Отображает панель отладки Tracy с полезной информацией (например, SQL-запросы, время выполнения, использование памяти). + - При возникновении ошибки показывает подробную страницу ошибки с трассировкой вызовов функций и содержимым переменных. + - Автоматически обновляет кэш при изменении шаблонов Latte, файлов конфигурации и т. д. + + +🚀 Производственный режим: + - Не отображает никакой отладочной информации; все ошибки записываются в журнал. + - При возникновении ошибки показывает `ErrorPresenter` или общую страницу "Ошибка сервера". + - Кэш никогда не обновляется автоматически! + - Оптимизирован для скорости и безопасности. -Выбор режима осуществляется путем автоопределения, поэтому обычно нет необходимости настраивать или переключать что-либо вручную. Режим разработки используется, если приложение запущено на localhost (т. е. IP-адрес `127.0.0.1` или `::1`) и отсутствует прокси-сервер (т. е. его HTTP-заголовок). В противном случае приложение работает в производственном режиме. + +Режим определяется автоматически, поэтому в большинстве случаев нет необходимости настраивать или переключать его вручную: + +- Режим разработки: Активен на localhost (IP-адрес `127.0.0.1` или `::1`), если не используется прокси (т.е. на основании HTTP-заголовков). +- Производственный режим: Активен везде. Если вы хотите включить режим разработки в других случаях, например, для программистов, получающих доступ с определенного IP-адреса, вы можете использовать `setDebugMode()`: ```php -$configurator->setDebugMode('23.75.345.200'); // один или более IP-адресов +$this->configurator->setDebugMode('23.75.345.200'); // один или более IP-адресов ``` Мы определенно рекомендуем сочетать IP-адрес с файлом cookie. Мы будем хранить секретный токен в cookie `nette-debug', например, `secret1234`, и режим разработки будет активирован для программистов с такой комбинацией IP и cookie. ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` Можно полностью отключить режим разработчика, даже для localhost: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` Обратите внимание, что значение `true` жестко включает режим разработчика, чего никогда не должно происходить на рабочем сервере. @@ -90,7 +131,7 @@ $configurator->setDebugMode(false); Для облегчения отладки мы включим замечательный инструмент [Tracy |tracy:]. В режиме разработчика он визуализирует ошибки, а в режиме производства — записывает ошибки в указанный каталог: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ $configurator->enableTracy($appDir . '/log'); Nette использует кэш для DI-контейнера, RobotLoader, шаблонов и т. д. Поэтому необходимо задать путь к директории, где будет храниться кэш: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` В Linux или macOS установите [права на запись |nette:troubleshooting#Setting-Directory-Permissions] для каталогов `log/` и `temp/`. @@ -112,7 +153,7 @@ RobotLoader Обычно мы хотим автоматически загружать классы с помощью [RobotLoader |robot-loader:], поэтому мы должны запустить его и позволить ему загрузить классы из каталога, в котором находится `Bootstrap.php` (т. е. `__DIR__`) и все его подкаталоги: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ $configurator->createRobotLoader() Configurator позволяет указать часовой пояс для вашего приложения. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ $configurator->setTimeZone('Europe/Prague'); Файлы конфигурации загружаются с помощью `addConfig()`: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` Метод `addConfig()` может быть вызван несколько раз для добавления нескольких файлов. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ if (PHP_SAPI === 'cli') { Параметры, используемые в файлах конфигурации, могут быть определены [в секции `parameters`|dependency-injection:configuration#parameters] и подхвачены (или перезаписаны) методом `addStaticParameters()` (у него есть алиас `addParameters()`). Важно, что разные значения параметров вызывают генерацию дополнительных DI-контейнеров, то есть дополнительных классов. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ $configurator->addStaticParameters([ Можно также добавить динамические параметры в контейнер. Их разные значения, в отличие от статических параметров, не приведут к генерации новых DI-контейнеров. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ Переменные среды могут быть легко доступны с использованием динамических параметров. Мы можем получить доступ к ним через `%env.variable%` в файлах конфигурации. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ $configurator->addDynamicParameters([ - `%wwwDir%` - абсолютный путь к директории, содержащей входной файл `index.php` - `%tempDir%` - абсолютный путь к директории для временных файлов. - `%vendorDir%` - абсолютный путь к директории, в которую Composer устанавливает библиотеки. +- `%rootDir%` - абсолютный путь к корневому каталогу проекта. - `%debugMode%` указывает, находится ли приложение в режиме отладки - `%consoleMode%` указывает, поступил ли запрос через командную строку @@ -225,7 +268,7 @@ services: Создаём новый экземпляр и вставляем его в Bootstrap: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Разные среды .[#toc-different-environments] =========================================== -Не стесняйтесь настроить класс `Bootstrap` в соответствии с вашими потребностями. Вы можете добавлять параметры в метод `boot()` для разделения веб-проектов, или добавлять другие методы, такие как `bootForTests()`, которые инициализируют среду для модульных тестов, `bootForCli()` для скриптов, вызываемых из командной строки, и так далее. +Не стесняйтесь настраивать класс `Bootstrap` в соответствии с вашими потребностями. Вы можете добавить параметры в метод `bootWebApplication()`, чтобы различать веб-проекты. Также можно добавить другие методы, например `bootTestEnvironment()` для инициализации окружения для модульных тестов, `bootConsoleApplication()` для скриптов, вызываемых из командной строки, и так далее. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container +{ + Tester\Environment::setup(); // Инициализация тестера Nette Tester + $this->setupContainer(); + return $this->configurator->createContainer(); +} + +public function bootConsoleApplication(): Nette\DI\Container { - $configurator = self::boot(); - Tester\Environment::setup(); // Инициализация Nette Tester - return $configurator; + $this->configurator->setDebugMode(false); + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); } ``` diff --git a/application/ru/components.texy b/application/ru/components.texy index 65adfeff51..31c39481d0 100644 --- a/application/ru/components.texy +++ b/application/ru/components.texy @@ -230,6 +230,28 @@ $this->redirect(/* ... */); // делаем редирект ``` +Перенаправление после сигнала .[#toc-redirection-after-a-signal] +================================================================ + +После обработки сигнала компонента часто следует перенаправление. Эта ситуация похожа на ситуацию с формами - после отправки формы мы также делаем перенаправление, чтобы предотвратить повторную отправку данных при обновлении страницы в браузере. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Поскольку компонент - это многократно используемый элемент и обычно не должен иметь прямой зависимости от конкретных презентаторов, методы `redirect()` и `link()` автоматически интерпретируют параметр как сигнал компонента: + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +Если вам нужно перенаправить на другого ведущего или действие, вы можете сделать это через ведущего: + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Постоянные параметры .[#toc-persistent-parameters] ================================================== @@ -347,7 +369,7 @@ services: Наконец, мы будем использовать эту фабрику в нашем презентере: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ interface PollControlFactory Компоненты в Nette Application - это многократно используемые части веб-приложения, которые мы встраиваем в страницы, о чем и пойдет речь в этой главе. Каковы возможности такого компонента? 1) он может быть отображен в шаблоне -2) он знает, какую часть себя отображать во время [AJAX-запроса |ajax#invalidation] (сниппеты) +2) он знает [, какую часть себя от |ajax#snippets] рисовывать при AJAX-запросе (сниппеты) 3) он имеет возможность хранить свое состояние в URL (постоянные параметры) 4) имеет возможность реагировать на действия пользователя (сигналы) 5) создает иерархическую структуру (где корнем является ведущий) diff --git a/application/ru/configuration.texy b/application/ru/configuration.texy index 0216233dfd..22c2a59533 100644 --- a/application/ru/configuration.texy +++ b/application/ru/configuration.texy @@ -13,11 +13,15 @@ application: # отображает вкладку "Nette Application" на синем экране Tracy? debugger: ... # (bool) по умолчанию true - # будет ли вызываться презентер ошибок при ошибке? - catchExceptions: ... # (bool) по умолчанию true на «боевом» сервере + # будет ли вызываться error-presenter при ошибке? + # имеет значение только в режиме разработчика + catchExceptions: ... # (bool) по умолчанию равно true # имя презентера ошибок - errorPresenter: Error # (string) по умолчанию 'Nette:Error' + errorPresenter: Error # (string|array) по умолчанию 'Nette:Error' + + # определяет псевдонимы для ведущих и событий + aliases: ... # определяет правила для преобразования имени ведущего в класс mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) по умолчанию false ``` -Поскольку в режиме разработки презентеры ошибок по умолчанию не вызываются, а ошибки отображаются Tracy, изменение значения `catchExceptions` на `true` помогает проверить корректность работы презентеров ошибок во время разработки. +Начиная с версии `nette/application` 3.2 можно определить пару презентеров ошибок: + +```neon +application: + errorPresenter: + 4xx: Error4xx # для Nette\Application\BadRequestException + 5xx: Error5xx # для других исключений +``` Опция `silentLinks` определяет, как Nette ведет себя в режиме разработчика, когда генерация ссылок не удается (например, из-за отсутствия презентера и т. д.). Значение по умолчанию `false` означает, что Nette запускает `E_USER_WARNING`. Установка значения `true` подавляет это сообщение об ошибке. В производственной среде всегда вызывается `E_USER_WARNING`. Мы также можем повлиять на это поведение, установив переменную презентера [$invalidLinkMode |creating-links#Invalid-Links]. -Карта [mapping определяет правила |modules#Mapping], по которым имя класса выводится из имени ведущего. +[Псевдонимы упрощают обращение к |creating-links#aliases] часто используемым ведущим. + +[Сопоставление определяет правила |directory-structure#Presenter Mapping], по которым имя класса выводится из имени ведущего. Авторегистрация презентеров .[#toc-automatic-registration-of-presenters] @@ -82,6 +95,9 @@ latte: # включает [проверку сгенерированного кода |latte:develop#Checking Generated Code] phpLinter: ... # (string) по умолчанию равно null + # устанавливает локаль + locale: cs_CZ # (string) по умолчанию null + # класс $this->template templateClass: App\MyTemplateClass # по умолчанию Nette\Bridges\ApplicationLatte\DefaultTemplate ``` @@ -91,7 +107,7 @@ latte: ```neon latte: расширения: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` /--comment diff --git a/application/ru/creating-links.texy b/application/ru/creating-links.texy index 3a4dbb4805..52e1782663 100644 --- a/application/ru/creating-links.texy +++ b/application/ru/creating-links.texy @@ -38,7 +38,7 @@ подробнее ``` -Если метод `ProductPresenter::renderShow()` не имеет `$lang` в своей сигнатуре, он может прочитать значение параметра, используя `$lang = $this->getParameter('lang')`. +Если метод `ProductPresenter::renderShow()` не имеет в своей сигнатуре `$lang`, то он может получить значение параметра с помощью `$lang = $this->getParameter('lang')` или из [свойства |presenters#Request Parameters]. Если параметры хранятся в массиве, их можно расширить с помощью оператора `(expand)` (что-то вроде оператора `...` в PHP, но работает с ассоциативными массивами): @@ -103,7 +103,7 @@ $url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); главная страница ``` -Ссылки могут также указывать на другие [модули |modules]. Здесь ссылки различаются на относительные по отношению к подмодулям или абсолютные. Принцип аналогичен дисковым путям, только вместо косых черт стоят двоеточия. Предположим, что настоящий презентер является частью модуля `Front`, тогда мы напишем: +Ссылки также могут указывать на другие [модули |directory-structure#Presenters and Templates]. Здесь ссылки различаются на относительные к подмодулям и абсолютные. Принцип аналогичен дисковым путям, только вместо слэшей используются двоеточия. Предположим, что настоящий ведущий является частью модуля `Front`, тогда мы напишем: ```latte ссылка на Front:Shop:Product:show @@ -140,7 +140,7 @@ $url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); обновить ``` -При этом передаются все параметры, указанные в сигнатуре метода `render()` или `action()`. Таким образом, если мы находимся на страницах `Product:show` и `id:123`, ссылка на `this` также будет передавать этот параметр. +В то же время все параметры, указанные в сигнатуре `action()` или `render()` метода, если `action()` не определены, передаются. Таким образом, если мы находимся на страницах `Product:show` и `id:123`, то ссылка на `this` также будет передавать этот параметр. Конечно, можно указать параметры напрямую: @@ -213,7 +213,7 @@ $url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); Если мы хотим сделать ссылку на презентеры в шаблоне компонента, мы используем тег `{plink}`: ```latte -главная страница +главная страница ``` or in the code @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Псевдонимы .[#toc-aliases]{data-version:v3.2.2} +=============================================== + +Иногда полезно назначить легко запоминающийся псевдоним для пары Ведущий:действие. Например, вы можете назвать домашнюю страницу `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 +``` + +В ссылках они записываются с использованием символа at, например: + +```latte +administration +``` + +Они поддерживаются во всех методах, работающих со ссылками, таких как `redirect()` и подобные. + + Недействительные ссылки .[#toc-invalid-links] ============================================= @@ -257,6 +281,6 @@ LinkGenerator LinkGenerator — это сервис, который можно передать через конструктор, а затем создать ссылки с помощью метода 'link()'. -Есть разница по сравнению с презентерами. LinkGenerator создает все ссылки как абсолютные URL-адреса. Кроме того, нет «текущего презентера», поэтому невозможно указать только имя действия 'link('default')' или относительные пути к модулям. +По сравнению с ведущими, разница есть. LinkGenerator создает все ссылки непосредственно как абсолютные URL. Кроме того, здесь нет "фактического ведущего", поэтому вы не можете просто указать имя действия `link('default')` в качестве цели или перечислить относительные пути к модулям. Недопустимые ссылки всегда выбросывают исключение `Nette\Application\UI\InvalidLinkException`. diff --git a/application/ru/directory-structure.texy b/application/ru/directory-structure.texy new file mode 100644 index 0000000000..4b36b5cf31 --- /dev/null +++ b/application/ru/directory-structure.texy @@ -0,0 +1,526 @@ +Структура каталогов приложения +****************************** + +
    + +Как разработать четкую и масштабируемую структуру каталогов для проектов на Nette Framework? Мы покажем вам проверенные практики, которые помогут вам организовать ваш код. Вы узнаете: + +- как **логически структурировать** приложение по каталогам +- как спроектировать структуру так, чтобы она **масштабировалась** по мере роста проекта +- каковы **возможные альтернативы** и их преимущества и недостатки + +
    + + +Важно отметить, что Nette Framework сам по себе не настаивает на какой-то определенной структуре. Она разработана таким образом, чтобы ее можно было легко адаптировать к любым потребностям и предпочтениям. + + +Базовая структура проекта .[#toc-basic-project-structure] +========================================================= + +Хотя 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. + + +Принципы организации кода .[#toc-code-organization-principles] +============================================================== + +Когда вы впервые изучаете новый проект, вы должны быть в состоянии быстро сориентироваться. Представьте, что вы перешли в каталог `app/Model/` и увидели такую структуру: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +Из нее вы узнаете только то, что проект использует некоторые сервисы, репозитории и сущности. Вы ничего не узнаете о реальном назначении приложения. + +Давайте рассмотрим другой подход - **организацию по доменам**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Это совсем другое дело - с первого взгляда понятно, что это сайт электронной коммерции. Сами названия каталогов говорят о том, что умеет делать приложение - оно работает с платежами, заказами и товарами. + +Первый подход (организация по типу класса) на практике приводит к нескольким проблемам: логически связанный код разбросан по разным папкам, и вам приходится прыгать между ними. Поэтому мы будем организовывать по доменам. + + +Пространства имен .[#toc-namespaces] +------------------------------------ + +Принято считать, что структура каталогов соответствует пространствам имен в приложении. Это означает, что физическое расположение файлов соответствует их пространству имен. Например, класс, расположенный в каталоге `app/Model/Product/ProductRepository.php`, должен иметь пространство имен `App\Model\Product`. Этот принцип помогает ориентироваться в коде и упрощает автозагрузку. + + +Единственное и множественное число в именах .[#toc-singular-vs-plural-in-names] +------------------------------------------------------------------------------- + +Обратите внимание, что мы используем единственное число для основных каталогов приложения: `app`, `config`, `log`, `temp`, `www`. То же самое относится и к внутренним каталогам приложения: `Model`, `Core`, `Presentation`. Это связано с тем, что каждый из них представляет собой единую концепцию. + +Аналогично, `app/Model/Product` представляет все, что связано с продуктами. Мы не называем его `Products`, потому что это не папка с продуктами (в ней были бы такие файлы, как `iphone.php`, `samsung.php`). Это пространство имен, содержащее классы для работы с продуктами - `ProductRepository.php`, `ProductService.php`. + +Папка `app/Tasks` имеет множественное число, потому что содержит набор отдельных исполняемых скриптов - `CleanupTask.php`, `ImportTask.php`. Каждый из них является самостоятельной единицей. + +Для согласованности мы рекомендуем использовать: +- Единственное число для пространств имен, представляющих функциональную единицу (даже если вы работаете с несколькими сущностями). +- Множественное число для коллекций независимых единиц. +- В случае неопределенности или если вы не хотите об этом думать, выбирайте единственное число + + +Публичный каталог `www/` .[#toc-public-directory-www] +===================================================== + +Эта директория является единственной, доступной из Интернета (так называемый document-root). Вы можете часто встретить название `public/` вместо `www/` - это просто условность и не влияет на функциональность. Каталог содержит: +- [Точка входа в |bootstrap#index.php] приложение `index.php` +- Файл `.htaccess` с правилами mod_rewrite (для Apache) +- Статические файлы (CSS, JavaScript, изображения) +- Загружаемые файлы + +Для обеспечения безопасности приложения очень важно правильно [настроить document-root |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url]. + +.[note] +Никогда не размещайте папку `node_modules/` в этом каталоге - она содержит тысячи файлов, которые могут быть исполняемыми и не должны находиться в открытом доступе. + + +Каталог приложений `app/` .[#toc-application-directory-app] +=========================================================== + +Это основной каталог с кодом приложения. Основная структура: + +/--pre +app/ +├── Core/ ← инфраструктурные вопросы +├── Model/ ← бизнес-логика +├── Presentation/ ← презентаторы и шаблоны +├── Tasks/ ← командные скрипты +└── Bootstrap.php ← класс загрузки приложения +\-- + +`Bootstrap.php` [класс запуска приложения |bootstrap], который инициализирует окружение, загружает конфигурацию и создает DI-контейнер. + +Теперь рассмотрим отдельные подкаталоги подробнее. + + +Презентаторы и шаблоны .[#toc-presenters-and-templates] +======================================================= + +Презентационная часть приложения находится в каталоге `app/Presentation`. Альтернативой может служить короткий `app/UI`. Это место для всех презентаторов, их шаблонов и любых вспомогательных классов. + +Мы организуем этот слой по доменам. В сложном проекте, объединяющем электронную коммерцию, блог и 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, карты сайта и т.д. +\-- + +Папки типа `Home/` или `Dashboard/` содержат ведущие и шаблоны. Папки типа `Front/`, `Admin/` или `Api/` называются **модулями**. Технически это обычные каталоги, которые служат для логической организации приложения. + +Каждая папка с ведущим содержит аналогичный ведущий и его шаблоны. Например, папка `Dashboard/` содержит: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← ведущий +└── default.latte ← шаблон +\-- + +Эта структура каталогов отражается в пространствах имен классов. Например, `DashboardPresenter` находится в пространстве имен `App\Presentation\Admin\Dashboard` (см. [сопоставление с ведущим |#presenter mapping]): + +```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`. + + +Разработка гибкой структуры .[#toc-flexible-structure-development] +------------------------------------------------------------------ + +Одно из главных преимуществ этой структуры - то, как элегантно она адаптируется к растущим потребностям проекта. В качестве примера возьмем часть, генерирующую XML-ленты. Изначально у нас есть простая форма: + +/--pre +Export/ +├── ExportPresenter.php ← один ведущий для всех экспортов +├── sitemap.latte ← шаблон для карты сайта +└── feed.latte ← шаблон для RSS-ленты +\-- + +Со временем добавляются новые типы фидов, и нам нужна дополнительная логика для них... Нет проблем! Папка `Export/` просто становится модулем: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← фид для Amazon + └── ebay.latte ← фид для eBay +\-- + +Эта трансформация совершенно гладкая - просто создайте новые подпапки, разделите на них код и обновите ссылки (например, с `Export:feed` на `Export:Feed:amazon`). Благодаря этому мы можем постепенно расширять структуру по мере необходимости, уровень вложенности никак не ограничен. + +Например, если в администрировании у вас много презентеров, связанных с управлением заказами, таких как `OrderDetail`, `OrderEdit`, `OrderDispatch` и т. д., для лучшей организации можно создать модуль (папку) `Order`, который будет содержать (папки для) презентеров `Detail`, `Edit`, `Dispatch` и других. + + +Расположение шаблона .[#toc-template-location] +---------------------------------------------- + +В предыдущих примерах мы видели, что шаблоны располагаются непосредственно в папке с ведущим: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← ведущий +├── DashboardTemplate.php ← дополнительный класс шаблона +└── default.latte ← шаблон +\-- + +Такое расположение оказывается наиболее удобным на практике - все связанные с ними файлы оказываются под рукой. + +В качестве альтернативы вы можете поместить шаблоны в подпапку `templates/`. Nette поддерживает оба варианта. Можно даже разместить шаблоны совсем вне папки `Presentation/`. Все, что касается вариантов размещения шаблонов, вы найдете в главе ["Поиск шаблонов" |templates#Template Lookup]. + + +Классы-помощники и компоненты .[#toc-helper-classes-and-components] +------------------------------------------------------------------- + +Презентаторы и шаблоны часто поставляются с другими вспомогательными файлами. Мы размещаем их логически в соответствии с их областью применения: + +1. **Непосредственно с презентатором** в случае специфических компонентов для данного презентатора: + +/--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`. Выбор зависит от командных соглашений. + + +Модель - сердце приложения .[#toc-model-heart-of-the-application] +================================================================= + +Модель содержит всю бизнес-логику приложения. Для ее организации действует то же правило - мы структурируем по доменам: + +/--pre +app/Model/ +├── Payment/ ← все о платежах +│ ├── PaymentFacade.php ← основная точка входа +│ ├── PaymentRepository.php +│ ├── Payment.php ← сущность +├── Order/ ← всё о заказах +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← всё о доставке +\-- + +В модели обычно встречаются такие типы классов: + +**Фасады**: представляют собой основную точку входа в определенный домен приложения. Они выступают в роли оркестратора, который координирует взаимодействие между различными сервисами для реализации законченных сценариев использования (например, "создать заказ" или "обработать платеж"). Под своим оркестровым слоем фасад скрывает детали реализации от остальной части приложения, обеспечивая тем самым чистый интерфейс для работы с данным доменом. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // Проверка + // создание заказа + // отправка писем + // запись в статистику + } +} +``` + +**Сервисы**: фокусируются на конкретных бизнес-операциях внутри домена. В отличие от фасадов, которые организуют целые сценарии использования, сервис реализует конкретную бизнес-логику (например, расчет цен или обработку платежей). Сервисы, как правило, не имеют статического состояния и могут использоваться либо фасадами как строительные блоки для более сложных операций, либо непосредственно другими частями приложения для более простых задач. + +```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 +// Сущность, сопоставленная с таблицей базы данных "Заказы +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, + ]); + } +} +``` + +**Объекты значений**: неизменяемые объекты, представляющие значения без собственной идентичности - например, денежную сумму или адрес электронной почты. Два экземпляра объекта-значения с одинаковыми значениями считаются идентичными. + + +Код инфраструктуры .[#toc-infrastructure-code] +============================================== + +Папка `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` для отправки писем о заказах, знает их шаблоны и время отправки + + +Командные скрипты .[#toc-command-scripts] +========================================= + +Приложениям часто требуется выполнять задачи, выходящие за рамки обычных 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-запрос, но при этом необходимо учитывать безопасность. Ведущий, запускающий задачу, должен быть защищен, например, только для вошедших в систему пользователей или с помощью надежного токена и доступа с разрешенных IP-адресов. Для длительных заданий необходимо увеличить лимит времени выполнения сценария и использовать `session_write_close()`, чтобы избежать блокировки сессии. + + +Другие возможные каталоги .[#toc-other-possible-directories] +============================================================ + +Помимо перечисленных основных директорий, вы можете добавить другие специализированные папки в зависимости от потребностей проекта. Давайте рассмотрим наиболее распространенные из них и их использование: + +/--pre +app/ +├── Api/ ← Логика API не зависит от уровня представления +├── Database/ ← скрипты миграции и семплы для тестовых данных +├── Components/ ← общие визуальные компоненты для всего приложения +├── Event/ ← полезно при использовании событийно-управляемой архитектуры +├── Mail/ ← шаблоны электронной почты и связанная с ними логика +└── Utils/ ← вспомогательные классы +\-- + +Для общих визуальных компонентов, используемых в презентаторах всего приложения, вы можете использовать папку `app/Components` или `app/Controls`: + +/--pre +app/Components/ +├── Form/ ← общие компоненты формы +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← компоненты для списков данных +│ └── DataGrid.php +└── Navigation/ ← элементы навигации + ├── Breadcrumbs.php + └── Menu.php +\-- + +Сюда помещаются компоненты с более сложной логикой. Если вы хотите использовать компоненты в нескольких проектах, лучше выделить их в отдельный пакет композитора. + +В директории `app/Mail` вы можете разместить управление почтовыми коммуникациями: + +/--pre +app/Mail/ +├── templates/ ← шаблоны электронной почты +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Составление карты ведущего .[#toc-presenter-mapping] +==================================================== + +Mapping определяет правила получения имен классов из имен ведущих. Мы указываем их в [конфигурации |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 +``` + +Сопоставление происходит путем замены звездочки в маске `App\Presentation\*Presenter` на имя ведущего `Home`, в результате чего получается конечное имя класса `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/ru/how-it-works.texy b/application/ru/how-it-works.texy index 02bdce2b15..f055d6e130 100644 --- a/application/ru/how-it-works.texy +++ b/application/ru/how-it-works.texy @@ -22,18 +22,18 @@ /--pre web-project/ ├── app/ ← каталог с приложением -│ ├── Presenters/ ← классы презентеров -│ │ ├── HomePresenter.php ← Класс презентера главной страницы -│ │ └── templates/ ← директория шаблонов -│ │ ├── @layout.latte ← шаблон общего макета -│ │ └── Home/ ← шаблоны презентера главной страницы -│ │ └── default.latte ← шаблон действия `default` -│ ├── Router/ ← конфигурация URL-адресов +│ ├── Core/ ← основные необходимые классы +│ │ └── RouterFactory.php ← настройка URL-адресов +│ ├── Presentation/ ← презентаторы, шаблоны и др. +│ │ ├── @layout.latte ← шаблон общего макета +│ │ └── Home/ ← Главная директория ведущих +│ │ ├── HomePresenter.php ← Класс презентера дома +│ │ └── default.latte ← шаблон для действий default │ └── Bootstrap.php ← загрузочный класс Bootstrap ├── bin/ ← скрипты командной строки ├── config/ ← файлы конфигурации │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← журналы ошибок ├── temp/ ← временные файлы, кэш, … ├── vendor/ ← библиотеки, установленные через Composer @@ -45,9 +45,9 @@ └── .htaccess ← запрещает доступ ко всем каталогам, кроме www \-- -Вы можете изменить структуру каталогов любым способом, переименовать или переместить папки, а затем просто отредактировать пути к `log/` и `temp/` в файле `Bootstrap.php` и путь к этому файлу в `composer.json` в секции `autoload`. Ничего больше, никакой сложной перенастройки, никаких постоянных изменений. Nette имеет [интеллектуальное автоопределение|bootstrap#development-vs-production-mode]. +Вы можете изменять структуру каталогов по своему усмотрению, переименовывать или перемещать папки - это абсолютно гибкая система. Nette также обладает интеллектуальной функцией автоопределения и автоматически распознает местоположение приложения, включая его URL. -Для немного больших приложений мы можем разделить папки с ведущими и шаблонами на подкаталоги (на диске) и на пространства имен (в коде), которые мы называем [модулями|modules]. +Для немного больших приложений мы можем организовать папки с ведущими и шаблонами в [подкаталоги |directory-structure#Presenters and templates] и сгруппировать классы в пространства имен, которые мы называем модулями. Публичный каталог `www/` может быть изменен без необходимости устанавливать что-либо ещё. На самом деле, часто бывает, что из-за специфики вашего хостинга вам придется переименовать его или, наоборот, установить так называемый document-root на этот каталог в конфигурации хостинга. Если ваш хостинг не позволяет создавать папки на один уровень выше публичного каталога, советуем вам поискать другой хостинг. В противном случае вы подвергнете себя значительному риску безопасности. @@ -75,7 +75,7 @@ HTTP-запрос .[#toc-http-request] Что за фабрика? Мы производим не тракторы, а веб-сайты! Подождите, сейчас всё будет объяснено. -Под «инициализацией среды» подразумевается, например, что активирован сервис [Tracy |tracy:], который является удивительным инструментом для регистрации или визуализации ошибок. Он регистрирует ошибки на рабочем сервере и отображает их непосредственно на сервере разработки. Поэтому при инициализации также необходимо решить, работает ли сайт в производственном режиме или в режиме разработчика. Для этого Nette использует автоопределение: если вы запускаете сайт на localhost, он работает в режиме разработчика. Вам не нужно ничего настраивать, и приложение готово как для разработки, так и для производственного развертывания. Эти шаги выполняются и подробно описываются в главе [Bootstrap |bootstrap]. +Под "инициализацией среды" мы подразумеваем, например, активацию [Tracy |tracy:], который является фантастическим инструментом для ведения логов и визуализации ошибок. На производственных серверах он записывает ошибки в журнал, а на разработках - отображает их напрямую. Таким образом, инициализация включает в себя определение того, в каком режиме работает сайт - в производственном или в режиме разработки. Для этого Nette использует [интеллектуальное автоопределение |bootstrap#development-vs-production-mode]: если вы запускаете сайт на localhost, он работает в режиме разработки. Конфигурация не требуется, и приложение готово как к разработке, так и к развертыванию на производстве. Эти шаги выполняются и подробно описываются в главе " [Класс Bootstrap |bootstrap] ". Третий пункт (да, мы пропустили второй, но мы к нему вернемся) — это запуск приложения. Обработкой HTTP-запросов в Nette занимается класс `Nette\Application\Application` (далее `Application`), поэтому когда мы говорим «запустить приложение», мы подразумеваем вызов метода с именем `run()` на объекте этого класса. @@ -91,7 +91,7 @@ Nette — это наставник, который направляет вас Приложение начинает работу с того, что просит так называемый маршрутизатор решить, какому из презентеров передать текущий запрос на обработку. Маршрутизатор решает, чья это ответственность. Он просматривает входной URL `https://example.com/product/123` и, основываясь на том, как он настроен, решает, что это задание, например, для **презентера** `Product`, который хочет `показать` продукт с `id: 123` как действие. Хорошей привычкой является написание пар презентер + действие, разделенных двоеточием: `Продукт:показать`. -Поэтому маршрутизатор преобразовал URL в пару `Presenter:action` + параметры, в нашем случае `Product:show` + `id`: 123`. Вы можете увидеть, как выглядит маршрутизатор в файле `app/Router/RouterFactory.php`, и мы подробно опишем его в главе [Маршрутизация |routing]. +Поэтому маршрутизатор преобразовал URL в пару `Presenter:action` + параметры, в нашем случае `Product:show` + `id`: 123`. Вы можете увидеть, как выглядит маршрутизатор в файле `app/Core/RouterFactory.php`, и мы подробно опишем его в главе [Маршрутизация |routing]. Давайте двигаться дальше. Приложение уже знает имя презентера и может продолжить работу. Путем создания объекта `ProductPresenter`, который является кодом презентера `Product`. Точнее, он просит контейнер DI создать презентера, потому что создание объектов — это его работа. @@ -121,12 +121,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter После этого презентер возвращает ответ. Это может быть HTML-страница, изображение, XML-документ, отправка файла с диска, JSON или перенаправление на другую страницу. Важно отметить, что если мы явно не указываем, как реагировать (что имеет место в случае с `ProductPresenter`), ответом будет отображение шаблона с HTML-страницей. Почему? Ну, потому что в 99% случаев мы хотим отобразить шаблон, поэтому презентер принимает такое поведение по умолчанию и хочет облегчить нашу работу. Это точка зрения Nette. -Нам даже не нужно указывать, какой шаблон нужно вывести, он сам выводит путь к нему в соответствии с простой логикой. В случае с презентером `Product` и действием `show`, он пытается проверить, существует ли один из этих файлов шаблонов относительно каталога, в котором находится класс `ProductPresenter`: +Нам даже не нужно указывать, какой шаблон рендерить, фреймворк сам определит путь. В случае с действием `show` он просто попытается загрузить шаблон `show.latte` в директории с классом `ProductPresenter`. Он также попытается найти макет в файле `@layout.latte` (подробнее о [поиске шаблонов |templates#Template Lookup]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -И затем он отображает шаблон. Теперь задача презентера и всего приложения выполнена. Если шаблон не существует, будет возвращена страница с ошибкой 404. Подробнее о презентерах вы можете прочитать на странице [Презентеры |presenters]. +Затем происходит рендеринг шаблонов. На этом задача ведущего и всего приложения завершена, и работа закончена. Если шаблон не существует, будет возвращена страница с ошибкой 404. Подробнее о презентерах можно прочитать на странице [Презентеры |presenters]. [* request-flow.svg *] @@ -137,7 +134,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter 3) маршрутизатор декодирует URL как пару `Home:default` 4) создается объект `HomePresenter` 5) вызывается метод `renderDefault()` (если существует) -6) шаблон `templates/Home/default.latte` с макетом `templates/@layout.latte` отрисован +6) шаблон `default.latte` с макетом `@layout.latte` отрисован Возможно, сейчас вы столкнулись с множеством новых понятий, но мы считаем, что они имеют смысл. Создавать приложения в Nette — проще простого. diff --git a/application/ru/modules.texy b/application/ru/modules.texy deleted file mode 100644 index b8d3e626c2..0000000000 --- a/application/ru/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Модули -****** - -.[perex] -В Nette модули представляют собой логические единицы, из которых состоит приложение. Они включают ведущие, шаблоны, возможно, компоненты и классы моделей. - -Одного компонента для презентаторов и одного для шаблонов будет недостаточно для реальных проектов. Наличие десятков файлов в одной папке по меньшей мере неорганизованно. Как выйти из этого положения? Мы просто разделяем их на подкаталоги на диске и на пространства имен в коде. И это именно то, что делают модули Nette. - -Поэтому давайте забудем о единой папке для ведущих и шаблонов и вместо этого создадим модули, например, `Admin` и `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← директория с модулями -│ ├── Admin/ ← модуль Admin -│ │ ├── Presenters/ ← его презентеры -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← модуль Front -│ └── Presenters/ ← его презентеры -│ └── ... -\-- - -Эта структура каталогов будет отражена в пространствах имен классов, так, например, `DashboardPresenter` будет находиться в пространстве `App\Modules\Admin\Presenters`: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Ведущий `Dashboard` внутри модуля `Admin` обозначается в приложении с помощью двойной точечной нотации как `Admin:Dashboard`, а его действие `default` обозначается как `Admin:Dashboard:default`. -А откуда Nette знает, что `Admin:Dashboard` представляет класс `App\Modules\Admin\Presenters\DashboardPresenter`? Мы говорим об этом, используя [отображение |#Mapping] в конфигурации. -Таким образом, приведенная структура не является фиксированной, и вы можете изменять ее по своему усмотрению. - -Модули, конечно, могут содержать все другие части, помимо презентаторов и шаблонов, такие как компоненты, классы моделей и т.д. - - -Вложенные модули .[#toc-nested-modules] ---------------------------------------- - -Модули не обязательно должны формировать только плоскую структуру, вы также можете создавать, например, подмодули: - -/--pre -app/ -├── Modules/ ← директория с модулями -│ ├── Blog/ ← модуль Blog -│ │ ├── Admin/ ← подмодуль Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← подмодуль Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← модуль Forum -│ │ └── ... -\-- - -Таким образом, модуль `Blog` разбивается на подмодули `Admin` и `Front`. И опять же это будет отражено в пространствах имен, которые будут `App\Modules\Blog\Admin\Presenters` и т.д. Ведущий `Dashboard` внутри подмодуля называется `Blog:Admin:Dashboard`. - -Ветвление может быть настолько глубоким, насколько вы захотите, поэтому вы можете создавать подмодули. - - -Создание ссылок .[#toc-creating-links] --------------------------------------- - -Ссылки в шаблонах ведущего являются относительными по отношению к текущему модулю. Таким образом, ссылка `Foo:default` ведет к ведущему `Foo` в том же модуле, что и текущий ведущий. Например, если текущим модулем является `Front`, то ссылка выглядит следующим образом: - -```latte -odkaz na Front:Product:show -``` - -Ссылка является относительной, даже если имя модуля является ее частью, тогда он считается подмодулем: - -```latte -odkaz na Front:Shop:Product:show -``` - -Абсолютные ссылки записываются аналогично абсолютным путям на диске, но с двоеточиями вместо косых черт. Таким образом, абсолютная ссылка начинается с двоеточия: - -```latte -odkaz na Admin:Product:show -``` - -Чтобы узнать, находимся ли мы в определенном модуле или подмодуле, мы используем функцию `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Маршрутизация .[#toc-routing] ------------------------------ - -См. [главу о маршрутизации |routing#modules]. - - -Составление карты .[#toc-mapping] ---------------------------------- - -Определяет правила, по которым имя класса выводится из имени ведущего. Мы записываем их в [конфигурацию |configuration] под ключом `application › mapping`. - -Начнем с примера, в котором не используются модули. Мы просто хотим, чтобы классы ведущего имели пространство имен `App\Presenters`. То есть мы хотим, чтобы ведущий, например, `Home` отображался на класс `App\Presenters\HomePresenter`. Этого можно достичь с помощью следующей конфигурации: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -Имя презентера заменяется звездочкой, и в результате получается название класса. Легко! - -Если мы разделим докладчиков на модули, то для каждого модуля у нас может быть свой маппинг: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Теперь презентер `Front:Home` определяется классом `App\Modules\Front\HomePresenter`, а презентер `Admin:Dashboard` — `App\AdminModule\DashboardPresenter`. - -Удобнее будет создать общее правило (звездочка), которое заменит первые два правила и добавит дополнительную звездочку только для модуля: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Но что если мы используем несколько вложенных модулей и у нас есть, например, ведущий `Admin:User:Edit`? В этом случае сегмент со звездочкой, представляющий модуль для каждого уровня, будет просто повторяться, и результатом будет класс `App\Modules\Admin\User\Presenters\EditPresenter`. - -Альтернативной нотацией является использование массива, состоящего из трех сегментов, вместо строки. Эта нотация эквивалентна предыдущей: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -Значение по умолчанию - `*: *Module\*Presenter`. diff --git a/application/ru/presenters.texy b/application/ru/presenters.texy index 70b5df9ace..ac48e3a344 100644 --- a/application/ru/presenters.texy +++ b/application/ru/presenters.texy @@ -60,7 +60,7 @@ class ArticlePresenter extends Nette\Application\UI\Presenter Важно, что `action()` вызывается перед `render()`, поэтому внутри него мы можем, возможно, изменить следующий ход жизненного цикла, т. е. изменить шаблон, который будет отображаться, а также метод `render()`, который будет вызываться, используя `setView('otherView')`. -В метод передаются параметры из запроса. Можно и рекомендуется указывать типы для параметров, например `actionShow(int $id, string $slug = null)` — если параметр `id` отсутствует или если он не является целым числом, презентер возвращает [ошибку 404|#Error-404-etc] и завершает операцию. +В метод передаются параметры из запроса. Можно и рекомендуется указывать типы для параметров, например `actionShow(int $id, ?string $slug = null)` — если параметр `id` отсутствует или если он не является целым числом, презентер возвращает [ошибку 404|#Error-404-etc] и завершает операцию. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ $this->redirect(/* ... */); Ошибка 404 и т. д. .[#toc-error-404-etc] ======================================== -Когда мы не можем выполнить запрос, потому что, например, статья, которую мы хотим отобразить, не существует в базе данных, мы выбросим ошибку 404, используя метод `error(string $message = null, int $httpCode = 404)`, который представляет HTTP-ошибку 404: +Когда мы не можем выполнить запрос, потому что, например, статья, которую мы хотим отобразить, не существует в базе данных, мы выбросим ошибку 404, используя метод `error(?string $message = null, int $httpCode = 404)`, который представляет HTTP-ошибку 404: ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Параметры запроса .[#toc-request-parameters] +============================================ + +Ведущий, как и каждый компонент, получает свои параметры из HTTP-запроса. Их значения могут быть получены с помощью метода `getParameter($name)` или `getParameters()`. Значения представляют собой строки или массивы строк, по сути, необработанные данные, полученные непосредственно из URL. + +Для большего удобства рекомендуется сделать параметры доступными через свойства. Для этого достаточно аннотировать их с помощью `#[Parameter]` атрибутом: + +```php +use Nette\Application\Attributes\Parameter; // эта строка очень важна + +class HomePresenter extends Nette\Application\UI\Presenter +{ + #[Parameter] + public string $theme; // должна быть публичной +} +``` + +Для свойств рекомендуется указывать тип данных (например, `string`). В этом случае Nette будет автоматически приводить значение на его основе. Значения параметров также могут быть [проверены |#Validation of Parameters]. + +При создании ссылки можно непосредственно задать значение параметров: + +```latte +click +``` + + Постоянные параметры .[#toc-persistent-parameters] ================================================== @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Если `$this->lang` имеет значение, например, `'en'`, то ссылки, созданные с помощью `link()` или `n:href`, также будут содержать параметр `lang=en`. И когда ссылка будет щелкнута, она снова станет `$this->lang = 'en'`. -Для свойств рекомендуется указывать тип данных (например, `string`), а также можно указать значение по умолчанию. Значения параметров могут быть [проверены |#Validation of Persistent Parameters]. +Для свойств рекомендуется указывать тип данных (например, `string`), а также можно указать значение по умолчанию. Значения параметров могут быть [проверены |#Validation of Parameters]. Постоянные параметры по умолчанию передаются между всеми действиями данного ведущего. Чтобы передать их между несколькими ведущими, необходимо определить их либо: @@ -307,18 +333,12 @@ class ProductPresenter extends Nette\Application\UI\Presenter Того, что мы показали до сих пор в этой главе, вероятно, будет достаточно. Следующие строки предназначены для тех, кто интересуется презентерами досконально и хочет знать всё. -Требования и параметры .[#toc-requirement-and-parameters] ---------------------------------------------------------- - -Запрос, обрабатываемый ведущим, представляет собой объект [api:Nette\Application\Request] и возвращается методом ведущего `getRequest()`. Он включает в себя массив параметров, и каждый из них принадлежит либо какому-то из компонентов, либо непосредственно ведущему (который на самом деле тоже является компонентом, хотя и специальным). Поэтому Nette перераспределяет параметры и передает их между отдельными компонентами (и ведущим), вызывая метод `loadState(array $params)`. Параметры могут быть получены методом `getParameters(): array`, индивидуально с помощью `getParameter($name)`. Значения параметров представляют собой строки или массивы строк, в основном это необработанные данные, полученные непосредственно из URL. +Валидация параметров .[#toc-validation-of-parameters] +----------------------------------------------------- +Значения [параметров запроса |#request parameters] и [постоянных параметров |#persistent parameters], получаемых из URL, записываются в свойства методом `loadState()`. При этом также проверяется соответствие типа данных, указанного в свойстве, в противном случае выдается ответ с ошибкой 404 и страница не отображается. -Валидация постоянных параметров .[#toc-validation-of-persistent-parameters] ---------------------------------------------------------------------------- - -Значения [постоянных параметров |#persistent parameters], полученных из URL, записываются в свойства методом `loadState()`. Он также проверяет, соответствует ли тип данных, указанный в свойстве, в противном случае выдается ошибка 404 и страница не отображается. - -Никогда не доверяйте слепо постоянным параметрам, так как они могут быть легко перезаписаны пользователем в URL. Например, так мы проверяем, входит ли `$this->lang` в число поддерживаемых языков. Хороший способ сделать это - переопределить метод `loadState()`, упомянутый выше: +Никогда не следует слепо доверять параметрам, так как они могут быть легко перезаписаны пользователем в URL. Например, так мы проверяем, входит ли `$this->lang` в число поддерживаемых языков. Хороший способ сделать это - переопределить метод `loadState()`, упомянутый выше: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,7 +361,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Сохранение и восстановление запроса .[#toc-save-and-restore-the-request] ------------------------------------------------------------------------ -Вы можете сохранить текущий запрос в сессии или восстановить его из сессии и позволить презентеру выполнить его снова. Это полезно, например, когда пользователь заполняет форму, а срок действия его логина истекает. Чтобы не потерять данные, перед перенаправлением на страницу регистрации мы сохраняем текущий запрос в сессию с помощью функции `$reqId = $this->storeRequest()`, которая возвращает идентификатор в виде короткой строки и передает его в качестве параметра презентеру для регистрации. +Запрос, который обрабатывает ведущий, представляет собой объект [api:Nette\Application\Request] и возвращается методом ведущего `getRequest()`. + +Вы можете сохранить текущий запрос в сессии или восстановить его из сессии и позволить ведущему выполнить его снова. Это удобно, например, когда пользователь заполняет форму, а его логин истекает. Чтобы не потерять данные, перед перенаправлением на страницу входа в систему мы сохраняем текущий запрос в сессии с помощью метода `$reqId = $this->storeRequest()`, который возвращает идентификатор в виде короткой строки и передает его в качестве параметра презентеру входа в систему. После входа в систему мы вызываем метод `$this->restoreRequest($reqId)`, который забирает запрос у сессии и пересылает его ей. Метод проверяет, что запрос был создан тем же пользователем, который сейчас вошел в систему. Если другой пользователь вошел в систему или ключ недействителен, он ничего не делает, и программа продолжает работу. @@ -362,7 +384,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Вы также можете вызвать канонизацию вручную с помощью метода `canonicalize()`, который, как и метод `link()`, получает в качестве аргументов презентера, действия и параметры. Он создает ссылку и сравнивает её с текущим URL. Если они отличаются, то происходит перенаправление на сгенерированную ссылку. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // перенаправляет, если $slug отличается от $realSlug @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Ограничение доступа с помощью `#[Requires]` .[#toc-access-restriction-using-requires]{data-version:3.2.2} +--------------------------------------------------------------------------------------------------------- + +Атрибут `#[Requires]` предоставляет расширенные возможности для ограничения доступа к ведущим и их методам. С его помощью можно указать HTTP-методы, потребовать AJAX-запросы, ограничить доступ к одному и тому же источнику и ограничить доступ только пересылкой. Атрибут может применяться как к классам ведущих, так и к отдельным методам, таким как `action()`, `render()`, `handle()`, и `createComponent()`. + +Вы можете указать эти ограничения: +- на методы HTTP: `#[Requires(methods: ['GET', 'POST'])]` +- требующих AJAX-запроса: `#[Requires(ajax: true)]` +- доступ только из одного источника: `#[Requires(sameOrigin: true)]` +- доступ только через переадресацию: `#[Requires(forward: true)]` +- ограничения на определенные действия: `#[Requires(actions: 'default')]` + +Подробнее см. в разделе [Как использовать Requires атрибут |best-practices:attribute-requires]. + + +Проверка метода HTTP .[#toc-http-method-check] +---------------------------------------------- + +В 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-запроса, который браузеры автоматически отправляют перед фактическим запросом, когда необходимо определить, разрешен ли запрос с точки зрения политики CORS (Cross-Origin Resource Sharing). Если разрешить этот метод, но не реализовать соответствующий ответ, это может привести к несоответствиям и потенциальным проблемам безопасности. + + Дальнейшее чтение .[#toc-further-reading] ========================================= diff --git a/application/ru/routing.texy b/application/ru/routing.texy index dd37c4fd4d..ab494cb7f2 100644 --- a/application/ru/routing.texy +++ b/application/ru/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Или мы можем использовать эту форму, обратите внимание на переписывание регулярного выражения проверки: +Для более детальной спецификации можно использовать еще более расширенную форму, в которой помимо значений по умолчанию можно задать другие свойства параметров, например, регулярное выражение для проверки (см. параметр `id` ): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Эти более подробные форматы полезны для добавления дополнительных метаданных. +Важно отметить, что если параметры, определенные в массиве, не включены в маску пути, их значения не могут быть изменены, даже с помощью параметров запроса, указанных после вопросительного знака в URL. Фильтры и переводы .[#toc-filters-and-translations] @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Модули .[#toc-modules] ---------------------- -Если у нас есть несколько маршрутов, принадлежащих одному модулю, мы можем использовать `withModule()` для их группировки: +Если у нас есть несколько маршрутов, относящихся к одному [модулю |directory-structure#Presenters and Templates], мы можем использовать `withModule()` для их группировки: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Интеграция .[#toc-integration] ============================== -Чтобы подключить наш маршрутизатор к приложению, мы должны сообщить о нем контейнеру DI. Самый простой способ - это подготовить фабрику, которая будет создавать объект маршрутизатора, и сообщить конфигурации контейнера, чтобы она его использовала. Допустим, мы напишем для этого метод `App\Router\RouterFactory::createRouter()`: +Чтобы подключить наш маршрутизатор к приложению, мы должны сообщить о нем контейнеру DI. Самый простой способ - это подготовить фабрику, которая будет создавать объект маршрутизатора, и сообщить конфигурации контейнера, чтобы она его использовала. Допустим, мы напишем для этого метод `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ class RouterFactory ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Любые зависимости, такие как подключение к базе данных и т.д., передаются методу фабрики в качестве параметров с помощью [autowiring |dependency-injection:autowiring]: @@ -663,7 +663,7 @@ $router->addRoute(/* ... */); Итак, мы снова добавим метод, который будет создавать, например, маршрутизатор: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Или мы будем создавать объекты напрямую: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/ru/templates.texy b/application/ru/templates.texy index 75f2c784d0..99d93b3e6f 100644 --- a/application/ru/templates.texy +++ b/application/ru/templates.texy @@ -34,35 +34,81 @@ Nette использует систему шаблонов [Latte |latte:]. Latt Он определяет блок `content`, который вставляется вместо `{include content}` в макете, а также переопределяет блок `title`, который перезаписывает `{block title}` в макете. Попытайтесь представить себе результат. -Поиск шаблонов .[#toc-search-for-templates] -------------------------------------------- +Поиск шаблонов .[#toc-template-lookup] +-------------------------------------- + +В презентаторах вам не нужно указывать, какой шаблон должен быть отображен; фреймворк автоматически определит путь, облегчая вам кодирование. + +Если вы используете структуру каталогов, в которой у каждого ведущего есть своя директория, просто поместите шаблон в эту директорию под именем действия (т. е. представления). Например, для действия `default` используйте шаблон `default.latte`: -Путь к шаблонам определяется ведущим с помощью простой логики. Он попытается проверить, есть ли один из этих файлов, расположенный относительно каталога класса ведущего, где `` это имя текущего ведущего, а `` это имя текущего события: +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -- `templates//.latte` -- `templates/..latte` +Если вы используете структуру, в которой ведущие находятся в одном каталоге, а шаблоны - в папке `templates`, сохраните их либо в файле `..latte` или `/.latte`: -Если шаблон не найден, он попытается выполнить поиск в каталоге `templates` на один уровень выше, т.е. на том же уровне, что и каталог с классом ведущего. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- -Если шаблон не найден и там, ответом будет [ошибка 404 |presenters#Error 404 etc.]. +Каталог `templates` можно также разместить на один уровень выше, на том же уровне, что и каталог с классами ведущих. -Вы также можете изменить вид с помощью `$this->setView('jineView')`. Или, вместо прямого поиска, укажите имя файла шаблона с помощью `$this->template->setFile('/path/to/template.latte')`. +Если шаблон не найден, ведущий отвечает [ошибкой 404 - страница не найдена |presenters#Error 404 etc]. + +Изменить вид можно с помощью `$this->setView('anotherView')`. Также можно напрямую указать файл шаблона с помощью `$this->template->setFile('/path/to/template.latte')`. .[note] -Файлы, в которых производится поиск шаблонов, можно изменить, наложив метод [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], который возвращает массив возможных имен файлов. +Файлы, в которых производится поиск шаблонов, можно изменить, переопределив метод [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], который возвращает массив возможных имен файлов. + + +Поиск шаблонов макета .[#toc-layout-template-lookup] +---------------------------------------------------- + +Nette также автоматически ищет файл макета. + +Если вы используете структуру каталогов, в которой у каждого ведущего есть своя директория, поместите макет либо в папку с ведущим, если он предназначен только для него, либо на уровень выше, если он общий для нескольких ведущих: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +Если вы используете структуру, в которой ведущие сгруппированы в одном каталоге, а шаблоны находятся в папке `templates`, макет будет находиться в следующих местах: -В этих файлах ожидается компоновка: +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` макет, общий для нескольких докладчиков +Если ведущий находится в модуле, он также будет искать дальше по дереву каталогов в соответствии с вложенностью модуля. -Где `` это имя текущего ведущего и `` это имя макета, которое по умолчанию равно `'layout'`. Имя может быть изменено с помощью `$this->setLayout('jinyLayout')`, поэтому будут опробованы файлы `@jinyLayout.latte`. +Имя макета можно изменить с помощью `$this->setLayout('layoutAdmin')`, и тогда оно будет ожидаться в файле `@layoutAdmin.latte`. Вы также можете напрямую указать файл шаблона макета с помощью `$this->setLayout('/path/to/template.latte')`. -Вы также можете напрямую указать имя файла шаблона макета с помощью `$this->setLayout('/path/to/template.latte')`. Использование `$this->setLayout(false)` отключает отслеживание макета. +Использование тега `$this->setLayout(false)` или `{layout none}` внутри шаблона отключает поиск макета. .[note] -Файлы, в которых производится поиск шаблонов макета, можно изменить, наложив метод [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], который возвращает массив возможных имен файлов. +Файлы, в которых производится поиск шаблонов макетов, можно изменить, переопределив метод [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], который возвращает массив возможных имен файлов. Переменные в шаблоне .[#toc-variables-in-the-template] @@ -104,7 +150,7 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template Вы также можете позволить себе роскошь шептать в шаблонах, просто установите плагин Latte в PhpStorm и поместите имя класса в начало шаблона, более подробную информацию смотрите в статье "Latte: как набирать систему":https://blog.nette.org/ru/latte-kak-ispol-zovat-sistemu-tipov: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void Latte версии 3 предлагает более продвинутый способ создания [расширения |latte:creating-extension] для каждого веб-проекта. Вот краткий пример такого класса: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ final class LatteExtension extends Latte\Extension ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ protected function beforeRender(): void ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` Транслятор можно использовать, например, в качестве фильтра `|translate`, передав дополнительные параметры в метод `translate()` (см. `foo, bar`): diff --git a/application/sl/@home.texy b/application/sl/@home.texy index e78407b83e..9fe0f8e8cd 100644 --- a/application/sl/@home.texy +++ b/application/sl/@home.texy @@ -2,35 +2,84 @@ Nette aplikacija **************** .[perex] -Paket `nette/application` je osnova za ustvarjanje interaktivnih spletnih aplikacij. +Nette Application je jedro ogrodja Nette, ki prinaša zmogljiva orodja za ustvarjanje sodobnih spletnih aplikacij. Ponuja številne izjemne funkcije, ki bistveno poenostavijo razvoj ter izboljšajo varnost in vzdrževanost kode. -- [Kako delujejo aplikacije |how-it-works]? -- [Bootstrap |Bootstrap] -- [Predstavitve |Presenters] -- [Predloge |Templates] -- [Moduli |Modules] -- [Usmerjanje |Routing] -- [Ustvarjanje povezav URL |creating-links] -- [Interaktivne komponente |components] -- [AJAX in sličice |ajax] -- [Multiplikator |multiplier] -- [Konfiguracija |Configuration] +Namestitev .[#toc-installation] +------------------------------- -Namestitev ----------- - -Prenesite in namestite paket s [programom Composer |best-practices:composer]: +Prenesite in namestite knjižnico s [programom Composer |best-practices:composer]: ```shell composer require nette/application ``` -| različica | združljivo s PHP + +Zakaj izbrati aplikacijo Nette? .[#toc-why-choose-nette-application] +-------------------------------------------------------------------- + +Nette je bil vedno pionir na področju spletnih tehnologij. + +**Dvosmerni usmerjevalnik:** Nette ima napreden sistem usmerjanja, ki je edinstven po svoji dvosmernosti - ne prevaja le URL-jev v dejanja aplikacije, temveč lahko URL-je ustvarja tudi v obratni smeri. To pomeni: +- strukturo URL-jev celotne aplikacije lahko kadar koli spremenite, ne da bi spreminjali datoteke predlog. +- URL-ji so samodejno kanonizirani, kar izboljša SEO +- Usmerjanje je opredeljeno na enem mestu in ni razpršeno v opombah + +**Komponente in signali:** Vgrajeni sistem komponent, ki se zgleduje po Delphiju in React.js, je edinstven med ogrodji PHP: +- Omogoča ustvarjanje elementov uporabniškega vmesnika za večkratno uporabo +- podpira hierarhično sestavo komponent +- ponuja elegantno obdelavo zahtevkov AJAX z uporabo signalov +- Bogata knjižnica pripravljenih komponent na spletnem mestu [Componette](https://componette.org). + +**AJAX in Snippets:** Nette je leta 2009 predstavil revolucionarni način dela z AJAX, še pred rešitvami, kot sta Hotwire za Ruby on Rails ali Symfony UX Turbo: +- Snippets omogočajo posodabljanje samo delov strani brez pisanja JavaScript +- Samodejna integracija s sistemom komponent +- Pametno razveljavljanje delov strani +- Minimalen prenos podatkov + +**Intuitivne predloge [Latte |latte:]:** Najbolj varen sistem za oblikovanje predlog za PHP z naprednimi funkcijami: +- Samodejna zaščita XSS s kontekstno občutljivim pobegom +- Razširljivost s filtri, funkcijami in oznakami po meri +- Dedovanje predlog in odlomki za AJAX +- Odlična podpora PHP 8.x s sistemom tipov + +**Vključevanje odvisnosti:** Nette v celoti uporablja vrivanje odvisnosti: +- Samodejno posredovanje odvisnosti (autowiring) +- Konfiguracija z uporabo jasnega formata NEON +- Podpora za tovarne komponent + + +Glavne prednosti .[#toc-main-benefits] +-------------------------------------- + +- **Varnost**: Samodejna zaščita pred [ranljivostmi, |nette:vulnerability-protection] kot so XSS, CSRF itd. +- **Produktivnost**: Manj pisanja, več funkcij zaradi pametne zasnove +- **Odstranjevanje napak**: [Tracy debugger |tracy:] z usmerjevalno ploščo +- **Zmogljivost**: Inteligentni sistem predpomnilnika, leno nalaganje komponent +- **Prilagodljivost**: Enostavno spreminjanje URL-jev tudi po zaključku aplikacije +- **Komponente**: Edinstven sistem elementov uporabniškega vmesnika za večkratno uporabo +- **Sodoben**: Popolna podpora za PHP 8.4+ in sistem tipov + + +Začetek .[#toc-getting-started] +------------------------------- + +1. [Razumevanje aplikacij |how-it-works] - Razumevanje osnovne arhitekture +2. [Predstavniki |presenters] - Delo s predstavniki in akcijami +3. [Predloge |templates] - Ustvarjanje predlog v Latte +4. [Usmerjanje |routing] - konfiguracija URL +5. [Interaktivne komponente |components] - uporaba sistema komponent + + +Združljivost s PHP .[#toc-php-compatibility] +-------------------------------------------- + +| različica | združljiv s PHP |-----------|------------------- -| Nette Application 4.0 | PHP 8.0 - 8.2 -| Nette Application 3.1 | PHP 7.2 - 8.2 -| Nette Application 3.0 | PHP 7.1 - 8.0 -| Nette Application 2.4 | PHP 5.6 - 8.0 +| 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 +| Aplikacija Nette 3.0 | PHP 7.1 - 8.0 +| Aplikacija Nette 2.4 | PHP 5.6 - 8.0 -Velja za najnovejše različice popravkov. +Velja za najnovejše različice popravkov. \ No newline at end of file diff --git a/application/sl/@left-menu.texy b/application/sl/@left-menu.texy index 4e23237948..570ef9271c 100644 --- a/application/sl/@left-menu.texy +++ b/application/sl/@left-menu.texy @@ -4,7 +4,7 @@ Nette aplikacija - [Bootstrap |Bootstrap] - [Predstavniki |Presenters] - [Predloge |Templates] -- [Moduli |Modules] +- [Struktura imenika |directory-structure] - [Usmerjanje |Routing] - [Ustvarjanje povezav URL |creating-links] - [Interaktivne komponente |components] diff --git a/application/sl/ajax.texy b/application/sl/ajax.texy index 1d982a2057..320e94b27c 100644 --- a/application/sl/ajax.texy +++ b/application/sl/ajax.texy @@ -3,10 +3,10 @@ AJAX in sličice
    -Sodobne spletne aplikacije danes tečejo pol na strežniku in pol v brskalniku. AJAX je ključni povezovalni dejavnik. Kakšno podporo ponuja ogrodje Nette? -- pošiljanje fragmentov predlog (tako imenovanih *snippets*) +V dobi sodobnih spletnih aplikacij, kjer je funkcionalnost pogosto razpeta med strežnikom in brskalnikom, je AJAX bistven povezovalni element. Katere možnosti ponuja ogrodje Nette na tem področju? +- pošiljanje delov predloge, tako imenovanih snippets - posredovanje spremenljivk med PHP in JavaScriptom -- razhroščevanje aplikacij AJAX +- orodja za razhroščevanje zahtevkov AJAX
    @@ -14,29 +14,32 @@ Sodobne spletne aplikacije danes tečejo pol na strežniku in pol v brskalniku. Zahteva AJAX .[#toc-ajax-request] ================================= -Zahteva AJAX se ne razlikuje od klasične zahteve - predvajalnik se pokliče z določenim pogledom in parametri. Prav tako je od predstavnika odvisno, kako se bo nanjo odzval: uporabi lahko svojo rutino, ki vrne fragment kode HTML (fragment HTML), dokument XML, objekt JSON ali kodo JavaScript. +Zahteva AJAX se v osnovi ne razlikuje od klasične zahteve HTTP. Predlagatelj se pokliče z določenimi parametri. Od predstavnika je odvisno, kako se bo odzval na zahtevo - lahko vrne podatke v obliki JSON, pošlje del kode HTML, dokument XML itd. -Na strani strežnika je mogoče zahtevo AJAX zaznati s storitveno metodo, ki [enkapsulira zahtevo HTTP |http:request] `$httpRequest->isAjax()` (zazna na podlagi glave HTTP `X-Requested-With`). Znotraj predstavnika je na voljo bližnjica v obliki metode `$this->isAjax()`. +Na strani brskalnika sprožimo zahtevo AJAX s funkcijo `fetch()`: -Na voljo je vnaprej obdelan objekt, imenovan `payload`, namenjen pošiljanju podatkov brskalniku v obliki JSON. - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // obdelava odziva +}); ``` -Za popoln nadzor nad izpisom JSON uporabite metodo `sendJson` v svojem predstavitvenem programu. S tem se takoj zaključi predstavitveni program, vi pa boste opravili brez predloge: +Na strani strežnika zahtevo AJAX prepozna metoda `$httpRequest->isAjax()` storitve, ki [enkapsulira zahtevo HTTP |http:request]. Uporablja glavo HTTP `X-Requested-With`, zato jo je nujno poslati. V predstavitvenem programu lahko uporabite metodo `$this->isAjax()`. + +Če želite poslati podatke v obliki JSON, uporabite metodo [`sendJson()` |presenters#Sending a response] metodo. Metoda tudi zaključi dejavnost predstavitvenega programa. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Če želimo poslati HTML, lahko nastavimo posebno predlogo za zahteve AJAX: +Če se nameravate odzvati s posebno predlogo, zasnovano za AJAX, lahko to storite na naslednji način: ```php public function handleClick($param): void @@ -44,27 +47,43 @@ public function handleClick($param): void if ($this->isAjax()) { $this->template->setFile('path/to/ajax.latte'); } - // ... + //... } ``` +Utrinki .[#toc-snippets] +======================== + +Najzmogljivejše orodje, ki ga ponuja Nette za povezovanje strežnika z odjemalcem, so snippets. Z njimi lahko navadno aplikacijo spremenite v aplikacijo AJAX z minimalnim naporom in nekaj vrsticami kode. Primer Fifteen prikazuje, kako vse to deluje, njegovo kodo pa lahko najdete na [GitHubu |https://github.com/nette-examples/fifteen]. + +Snippets ali clippings omogočajo posodabljanje samo delov strani, namesto da bi ponovno naložili celotno stran. To je hitrejše in učinkovitejše, poleg tega pa zagotavlja udobnejšo uporabniško izkušnjo. Snippets vas morda spominjajo na Hotwire za Ruby on Rails ali Symfony UX Turbo. Zanimivo pa je, da je Nette snippets predstavil že 14 let prej. + +Kako delujejo odlomki? Ob prvem nalaganju strani (zahteva, ki ni zahteva AJAX) se naloži celotna stran, vključno z vsemi snippets. Ko uporabnik komunicira s stranjo (npr. klikne gumb, odda obrazec itd.), se namesto nalaganja celotne strani izvede zahteva AJAX. Koda v predstavniku izvede dejanje in odloči, katere utrinke je treba posodobiti. Nette te utrinke prikaže in jih pošlje v obliki polja JSON. Obdelovalna koda v brskalniku nato vstavi prejete utrinke nazaj na stran. Zato se prenese samo koda spremenjenih izsekov, kar prihrani pasovno širino in pospeši nalaganje v primerjavi s prenosom celotne vsebine strani. + + Naja .[#toc-naja] -================= +----------------- - [Knjižnica Naja |https://naja.js.org] se uporablja za obdelavo zahtevkov AJAX na strani brskalnika. [Namestite |https://naja.js.org/#/guide/01-install-setup-naja] jo kot paket node.js (za uporabo s programi Webpack, Rollup, Vite, Parcel in drugimi): +Za obdelavo izsekov na strani brskalnika se uporablja [knjižnica Naja |https://naja.js.org]. [Namestite jo |https://naja.js.org/#/guide/01-install-setup-naja] kot paket node.js (za uporabo z aplikacijami, kot so Webpack, Rollup, Vite, Parcel in druge): ```shell npm install naja ``` -...ali pa jo vstavite neposredno v predlogo strani: +... ali jo vstavite neposredno v predlogo strani: ```html ``` -Če želite ustvariti zahtevo AJAX iz običajne povezave (signala) ali oddaje obrazca, preprosto označite ustrezno povezavo, obrazec ali gumb z razredom `ajax`: +Najprej morate knjižnico [inicializirati |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: + +```js +naja.initialize(); +``` + +Če želite, da običajna povezava (signal) ali oddaja obrazca postane zahteva AJAX, preprosto označite ustrezno povezavo, obrazec ali gumb z razredom `ajax`: ```html Go @@ -74,64 +93,39 @@ npm install naja or +
    ``` -. .[#toc-snippets] -================== - -Obstaja veliko močnejše orodje vgrajene podpore AJAX - snippets. Z njihovo uporabo je mogoče navadno aplikacijo spremeniti v aplikacijo AJAX z uporabo le nekaj vrstic kode. Kako vse to deluje, je prikazano v primeru Fifteen, katerega koda je na voljo tudi v sestavi ali na [GitHubu |https://github.com/nette-examples/fifteen]. - -Snippets delujejo tako, da se med začetno zahtevo (tj. zahtevo brez AJAX-a) prenese celotna stran, nato pa se pri vsaki [podpovprašitvi |components#signal] AJAX-a (zahteva istega pogleda istega predstavnika) v prej omenjeno shrambo `payload` prenese le koda spremenjenih delov. - -Snippets vas morda spominjajo na Hotwire za Ruby on Rails ali Symfony UX Turbo, vendar jih je Nette zasnoval že štirinajst let prej. - +Ponovno risanje utrinkov .[#toc-redrawing-snippets] +--------------------------------------------------- -Neveljavnost nizov (Snippets) .[#toc-invalidation-of-snippets] -============================================================== - -Vsak potomec razreda [Control |components] (kar je tudi Presenter) si lahko zapomni, ali je med zahtevo prišlo do kakšnih sprememb, zaradi katerih je treba ponovno prikazati. Za to obstaja par metod: `redrawControl()` in `isControlInvalid()`. Primer: +Vsak objekt razreda [Control |components] (vključno s samim Presenterjem) beleži, ali so se zgodile spremembe, ki zahtevajo njegovo ponovno izrisovanje. V ta namen se uporablja metoda `redrawControl()`. ```php public function handleLogin(string $user): void { - // Objekt se mora ponovno prikazati, ko se uporabnik prijavi + // po prijavi je treba ponovno narisati ustrezni del $this->redrawControl(); - // ... + //... } ``` -Nette pa ponuja še natančnejšo ločljivost kot celotne komponente. Navedeni metodi kot neobvezni parameter sprejmeta ime tako imenovanega "snippeta". "Snippet" je v bistvu element v vaši predlogi, ki je v ta namen označen z oznako Latte, več o tem pozneje. Tako je mogoče od komponente zahtevati, da na novo nariše samo *delčke* svoje predloge. Če je celotna komponenta razveljavljena, se ponovno izrišejo vse njene sličice. Komponenta je "neveljavna" tudi, če je neveljavna katera koli njena podkomponenta. - -```php -$this->isControlInvalid(); // -> false -$this->redrawControl('header'); // razveljavi odlomek z imenom 'header' -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, vsaj en delček je neveljaven +Nette omogoča tudi natančnejši nadzor nad tem, kaj je treba ponovno narisati. Zgoraj omenjena metoda lahko kot argument sprejme ime izseka. Tako je mogoče razveljaviti (kar pomeni: prisiliti k ponovnemu izrisu) na ravni dela predloge. Če je celotna komponenta razveljavljena, se na novo nariše tudi vsak njen delček: -$this->redrawControl(); // razveljavi celotno komponento, vsak odlomek -$this->isControlInvalid('footer'); // -> true +```php +// razveljavi odlomek 'header' +$this->redrawControl('header'); ``` -Komponenta, ki prejme signal, je samodejno označena za ponovno izrisovanje. - -Zahvaljujoč ponovnemu izrisu snippetov natančno vemo, katere dele katerih elementov je treba ponovno izrisati. - - -Oznaka `{snippet} … {/snippet}` .{toc: Tag snippet} -=================================================== - -Prikazovanje strani poteka zelo podobno kot pri običajni zahtevi: naložijo se iste predloge itd. Bistveno pa je, da se izpustijo deli, ki naj ne bi dosegli izpisa; drugi deli se povežejo z identifikatorjem in pošljejo uporabniku v obliki, razumljivi za obdelovalnik JavaScript. - -Sintaksa .[#toc-syntax] ------------------------ +Utrinki v Latte .[#toc-snippets-in-latte] +----------------------------------------- -Če je v predlogi kontrolni element ali izsek, ga moramo oviti z uporabo oznake `{snippet} ... {/snippet}` pair - ta bo poskrbela, da bo izrisani izsek "izrezan" in poslan brskalniku. Prav tako ga bo zaprla v pomožno vrstico `
    ` (možno je uporabiti tudi drugačno oznako). V naslednjem primeru je opredeljen izsek z imenom `header`. Prav tako lahko predstavlja predlogo komponente: +Uporaba snippetov v Latte je zelo preprosta. Če želite del predloge opredeliti kot izsek, ga preprosto ovijte v oznake `{snippet}` in `{/snippet}`: ```latte {snippet header} @@ -139,7 +133,9 @@ Sintaksa .[#toc-syntax] {/snippet} ``` -Snippet tipa, ki ni `
    ` ali izsek z dodatnimi atributi HTML se doseže z uporabo različice atributa: +Snippet ustvari element `
    ` na strani HTML s posebej ustvarjeno oznako `id`. Pri ponovnem izrisu sličice se vsebina tega elementa posodobi. Zato je treba ob začetnem izrisu strani izrisati tudi vse sličice, čeprav so lahko na začetku prazne. + +Fragment lahko ustvarite tudi z elementom, ki ni `
    ` z uporabo atributa n:: ```latte
    @@ -148,138 +144,106 @@ Snippet tipa, ki ni `
    ` ali izsek z dodatnimi atributi HTML se doseže z upo ``` -Dinamični utrinki .[#toc-dynamic-snippets] -========================================== +Območja sličic .[#toc-snippet-areas] +------------------------------------ -V programu Nette lahko na podlagi parametra v času izvajanja določite tudi dinamično ime snippetov. To je najbolj primerno za različne sezname, kjer moramo spremeniti samo eno vrstico, vendar ne želimo skupaj z njo prenesti celotnega seznama. Primer tega je: +Imena snippetov so lahko tudi izrazi: ```latte -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Obstaja en statični niz z imenom `itemsContainer`, ki vsebuje več dinamičnih nizov: `item-0`, `item-1` in tako naprej. +Tako bomo dobili več izsekov, kot so `item-0`, `item-1`, itd. Če bi neposredno razveljavili dinamični snippet (npr. `item-1`), se ne bi nič na novo izrisalo. Razlog za to je, da utrinki delujejo kot pravi izvlečki in se neposredno izrisujejo le oni sami. Vendar v predlogi tehnično ni nobenega odlomka z imenom `item-1`. Pojavi se šele ob izvajanju okoliške kode snippeta, v tem primeru zanke foreach. Zato bomo del predloge, ki ga je treba izvesti, označili z oznako `{snippetArea}`: -Dinamičnega utrinka ne morete narisati neposredno (ponovno narisanje `item-1` nima učinka), temveč morate narisati njegov nadrejeni utrinek (v tem primeru `itemsContainer`). To povzroči, da se izvede koda nadrejenega odlomka, vendar se nato brskalniku pošljejo samo njegovi pododlomki. Če želite poslati samo enega od podnapisov, morate spremeniti vnos za nadrejeni odlomek, da ne bo ustvaril drugih podnapisov. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -V zgornjem primeru morate poskrbeti, da bo za zahtevo AJAX v polje `$list` dodan samo en element, zato bo zanka `foreach` izpisala samo en dinamični odlomek. +In na novo bomo narisali tako posamezni izsek kot celotno krovno območje: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * This method returns data for the list. - * Usually this would just request the data from a model. - * For the purpose of this example, the data is hard-coded. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +Prav tako je treba zagotoviti, da polje `$items` vsebuje samo elemente, ki jih je treba ponovno narisati. -Utrinki v vključeni predlogi .[#toc-snippets-in-an-included-template] -===================================================================== - -Lahko se zgodi, da je snippet v predlogi, ki je vključena iz druge predloge. V tem primeru moramo vključitveno kodo v drugi predlogi oviti z oznako `snippetArea`, nato pa ponovno narišemo območje snippetArea in dejanski snippet. - -Oznaka `snippetArea` zagotavlja, da se koda v njej izvrši, vendar se brskalniku pošlje le dejanski odlomek iz vključene predloge. +Pri vstavljanju druge predloge v glavno s pomočjo oznake `{include}`, ki ima izseke, je treba vključeno predlogo ponovno zaviti v `snippetArea` in razveljaviti tako izsek kot območje skupaj: ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* included.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -Kombinirate jo lahko tudi z dinamičnimi utrinki. +Utrinki v sestavinah .[#toc-snippets-in-components] +--------------------------------------------------- -Dodajanje in brisanje .[#toc-adding-and-deleting] -================================================= - -Če na seznam dodate nov element in razveljavite `itemsContainer`, zahteva AJAX vrne odlomke, vključno z novim elementom, vendar ga obdelovalnik javascript ne bo mogel prikazati. Razlog za to je, da ni elementa HTML z novo ustvarjenim ID. - -V tem primeru je najpreprostejši način, da celoten seznam zavijete v še en izsek in ga razveljavite: +V [komponentah |components] lahko ustvarite sličice in Nette jih bo samodejno ponovno narisal. Vendar pa obstaja posebna omejitev: za ponovno izrisovanje snippetov je treba poklicati metodo `render()` brez parametrov. Tako posredovanje parametrov v predlogi ne bo delovalo: ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Pošiljanje uporabniških podatkov .[#toc-sending-user-data] +---------------------------------------------------------- + +Skupaj z odlomki lahko odjemalcu pošljete vse dodatne podatke. Preprosto jih zapišite v objekt `payload`: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + //... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -Enako velja za brisanje elementa. Možno bi bilo poslati prazen snippet, vendar so običajno seznami lahko oštevilčeni in bi bilo zapleteno izvesti brisanje enega elementa in nalaganje drugega (ki je bil prej na drugi strani oštevilčenega seznama). - -Pošiljanje parametrov sestavini .[#toc-sending-parameters-to-component] -======================================================================= +Pošiljanje parametrov .[#toc-sending-parameters] +================================================ -Ko komponenti prek zahteve AJAX pošiljamo parametre, bodisi signalne bodisi trajne, moramo navesti njihovo globalno ime, ki vsebuje tudi ime komponente. Polno ime parametra vrne metoda `getParameterId()`. +Ko komponenti z zahtevo AJAX pošiljamo parametre, bodisi signalne bodisi trajne, moramo navesti njihovo globalno ime, ki vsebuje tudi ime komponente. Polno ime parametra vrne metoda `getParameterId()`. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -In obdela metodo z ustreznimi parametri v komponenti. +Metoda handle z ustreznimi parametri v komponenti: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/sl/bootstrap.texy b/application/sl/bootstrap.texy index bc60e3cfe8..8d26452803 100644 --- a/application/sl/bootstrap.texy +++ b/application/sl/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + 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; + // Nastavite imenik za začasne datoteke, ki jih ustvari Nette (npr. sestavljene 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 { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // Program Nette je pameten in razvojni način se vklopi samodejno, + // lahko pa ga za določen naslov IP omogočite tako, da odkomentirate naslednjo vrstico: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Omogoči Tracy: najboljše orodje za razhroščevanje "švicarskega noža". + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: samodejno naloži vse razrede v danem imeniku + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Nalaganje konfiguracijskih datotek + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php .[#toc-index-php] =========================== -Pri spletnih aplikacijah je začetna datoteka `index.php`, ki se nahaja v javnem imeniku `www/`. Omogoča, da razred `Bootstrap` inicializira okolje in vrne `$configurator`, ki ustvari vsebnik DI. Nato pridobi storitev `Application`, ki izvaja spletno aplikacijo: +Pri spletnih aplikacijah je primarna datoteka `index.php`, ki se nahaja v [javnem imeniku |directory-structure#public-directory-www] `www/`. S tem bo razred Bootstrap inicializiral okolje in izdelal vsebnik DI. Iz njega nato pridobi storitev `Application`, ki zažene spletno aplikacijo: ```php -// inicializacija okolja + pridobitev predmeta Configurator -$configurator = App\Bootstrap::boot(); -// ustvarite vsebnik DI -$container = $configurator->createContainer(); +$bootstrap = new App\Bootstrap; +// Inicializacija okolja + ustvarjanje vsebnika DI +$container = $bootstrap->bootWebApplication(); // vsebnik DI ustvari objekt Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -// zaženite aplikacijo Nette +// Zagnati aplikacijo Nette in obdelati vhodno zahtevo $application->run(); ``` @@ -59,26 +84,42 @@ Kot vidite, razred [api:Nette\Bootstrap\Configurator], ki ga bomo zdaj podrobnej Razvojni in produkcijski način .[#toc-development-vs-production-mode] ===================================================================== -Nette razlikuje med dvema osnovnima načinoma izvajanja zahtevka: razvojnim in produkcijskim. Razvojni način je osredotočen na čim večje udobje programerja, prikazan je Tracy, predpomnilnik se samodejno posodablja ob spreminjanju predlog ali konfiguracije vsebnika DI itd. Produkcijski način je osredotočen na zmogljivost, Tracy beleži le napake, spremembe predlog in drugih datotek pa se ne preverjajo. +Nette se obnaša različno, odvisno od tega, ali deluje v razvojnem ali produkcijskem strežniku: + +🛠️ Razvojni način: + - Prikaže Tracyjev razhroščevalni niz z uporabnimi informacijami (npr. poizvedbe SQL, čas izvajanja, poraba pomnilnika). + - Prikaže podrobno stran z napakami s sledmi klicev funkcij in vsebino spremenljivk, ko pride do napake. + - Samodejno osveži predpomnilnik, ko se spremenijo predloge Latte, konfiguracijske datoteke itd. + + +🚀 Produkcijski način: + - Ne prikaže nobenih informacij o odpravljanju napak; vse napake se zabeležijo. + - Prikaže stran `ErrorPresenter` ali splošno stran "Server Error" (Napaka strežnika), ko pride do napake. + - Predpomnilnik se nikoli samodejno ne osveži! + - Optimizirano za hitrost in varnost. -Izbira načina poteka s samodejnim zaznavanjem, zato običajno ni treba ničesar ročno konfigurirati ali preklapljati. Način je razvojni, če aplikacija teče na lokalnem gostitelju (tj. naslov IP `127.0.0.1` ali `::1`) in ni prisoten posrednik (tj. njegova glavička HTTP). V nasprotnem primeru deluje v produkcijskem načinu. + +Način se določi samodejno, zato ga v večini primerov ni treba ročno konfigurirati ali preklapljati: + +- Razvojni način: (IP naslov `127.0.0.1` ali `::1`), razen če je v uporabi posrednik (tj. na podlagi glave HTTP). +- Produkcijski način: Aktivno povsod drugje. Če želite omogočiti razvojni način v drugih primerih, na primer za programerje, ki dostopajo z določenega naslova IP, lahko uporabite `setDebugMode()`: ```php -$configurator->setDebugMode('23.75.345.200'); // enega ali več naslovov IP. +$this->configurator->setDebugMode('23.75.345.200'); // enega ali več naslovov IP. ``` Vsekakor priporočamo kombinacijo naslova IP s piškotkom. V piškotek `nette-debug` bomo shranili tajni žeton, npr. `secret1234`, razvojni način pa bo aktiviran za programerje s to kombinacijo IP in piškotka. ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` Razvojni način lahko tudi popolnoma izklopimo, tudi za lokalni gostitelj: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` Vrednost `true` vklopi način za razvijalce, kar se na produkcijskem strežniku ne bi smelo zgoditi. @@ -90,7 +131,7 @@ Orodje za razhroščevanje Tracy .[#toc-debugging-tool-tracy] Za lažje razhroščevanje bomo vklopili odlično orodje [Tracy |tracy:]. V načinu za razvijalce vizualizira napake, v produkcijskem načinu pa napake beleži v določen imenik: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ Začasne datoteke .[#toc-temporary-files] Nette uporablja predpomnilnik za vsebnik DI, RobotLoader, predloge itd. Zato je treba nastaviti pot do imenika, v katerem bo shranjen predpomnilnik: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` V operacijskem sistemu Linux ali macOS nastavite [dovoljenja za pisanje za |nette:troubleshooting#Setting directory permissions] imenike `log/` in `temp/`. @@ -112,7 +153,7 @@ RobotLoader .[#toc-robotloader] Običajno bomo želeli samodejno naložiti razrede z [RobotLoaderjem |robot-loader:], zato ga moramo zagnati in mu omogočiti, da naloži razrede iz imenika, kjer se nahaja `Bootstrap.php` (tj. `__DIR__`), in vseh njegovih podimenikov: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ Druga možnost je, da uporabimo samo samodejno nalaganje [Composer |best-practic Configurator vam omogoča, da določite časovni pas za svojo aplikacijo. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ V razvojnem načinu se vsebnik samodejno posodobi vsakič, ko spremenite kodo al Konfiguracijske datoteke se naložijo z uporabo `addConfig()`: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` Metodo `addConfig()` lahko za dodajanje več datotek pokličete večkrat. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ Statični parametri .[#toc-static-parameters] Parametre, ki se uporabljajo v konfiguracijskih datotekah, je mogoče opredeliti [v razdelku `parameters` |dependency-injection:configuration#parameters] in jih tudi posredovati (ali prepisati) z metodo `addStaticParameters()` (ima vzdevek `addParameters()`). Pomembno je, da različne vrednosti parametrov povzročijo generiranje dodatnih vsebnikov DI, tj. dodatnih razredov. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ Dinamični parametri .[#toc-dynamic-parameters] Kontejnerju lahko dodamo tudi dinamične parametre, katerih različne vrednosti za razliko od statičnih parametrov ne bodo povzročile generiranja novih DI kontejnerjev. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ Spremenljivke okolja bi lahko preprosto dali na voljo z dinamičnimi parametri. Do njih lahko dostopamo prek spletne strani `%env.variable%` v konfiguracijskih datotekah. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ V konfiguracijskih datotekah lahko uporabite naslednje statične parametre: - `%wwwDir%` je absolutna pot do imenika, ki vsebuje vstopno datoteko `index.php` - `%tempDir%` je absolutna pot do imenika za začasne datoteke - `%vendorDir%` je absolutna pot do imenika, v katerega Composer namesti knjižnice +- `%rootDir%` je absolutna pot do korenskega imenika projekta - `%debugMode%` označuje, ali je aplikacija v načinu odpravljanja napak - `%consoleMode%` označuje, ali je bila zahteva poslana prek ukazne vrstice @@ -225,7 +268,7 @@ services: Ustvarite nov primerek in ga vstavite v bootstrap: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Različna okolja .[#toc-different-environments] ============================================== -Razred `Bootstrap` lahko prilagodite svojim potrebam. Metodi `boot()` lahko dodate parametre za razlikovanje med spletnimi projekti ali dodate druge metode, kot so `bootForTests()`, ki inicializira okolje za teste enote, `bootForCli()` za skripte, ki se kličejo iz ukazne vrstice, in tako naprej. +Ne oklevajte, če želite razred `Bootstrap` prilagoditi svojim potrebam. Metodi `bootWebApplication()` lahko dodate parametre za razlikovanje med spletnimi projekti. Lahko pa dodate tudi druge metode, na primer `bootTestEnvironment()` za inicializacijo okolja za teste enote, `bootConsoleApplication()` za skripte, ki se kličejo iz ukazne vrstice, in tako naprej. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container { - $configurator = self::boot(); Tester\Environment::setup(); // Inicializacija Nette Testerja - return $configurator; + $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 index 9947ac0b0f..344d660993 100644 --- a/application/sl/components.texy +++ b/application/sl/components.texy @@ -230,6 +230,28 @@ V predlogi so ta sporočila na voljo v spremenljivki `$flashes` kot objekti `std ``` +Preusmeritev po signalu .[#toc-redirection-after-a-signal] +========================================================== + +Po obdelavi signala komponente pogosto sledi preusmeritev. Ta situacija je podobna obrazcem - po oddaji obrazca prav tako preusmerimo, da preprečimo ponovno oddajo podatkov ob osvežitvi strani v brskalniku. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Ker je komponenta element za večkratno uporabo in običajno ne sme biti neposredno odvisna od določenih predstavnikov, metodi `redirect()` in `link()` samodejno interpretirata parameter kot signal komponente: + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +Če želite preusmeriti na drug predstavnik ali dejanje, lahko to storite prek predstavnika: + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Trajni parametri .[#toc-persistent-parameters] ============================================== @@ -347,7 +369,7 @@ services: Na koncu bomo to tovarno uporabili v našem predstavitvenem programu: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ Komponente poglobljeno .[#toc-components-in-depth] Komponente v aplikaciji Nette so ponovno uporabni deli spletne aplikacije, ki jih vgrajujemo v strani, kar je predmet tega poglavja. Kakšne natančno so zmožnosti takšne komponente? 1) mogoče jo je izrisati v predlogi -2) ve, kateri del sebe naj prikaže med [zahtevo AJAX |ajax#invalidation] (odlomki) +2) ve, [kateri del sebe |ajax#snippets] naj prikaže med zahtevo AJAX (fragmenti). 3) ima možnost shranjevanja svojega stanja v naslovu URL (trajni parametri). 4) ima sposobnost odzivanja na dejanja uporabnika (signali) 5) ustvarja hierarhično strukturo (kjer je korenski element predstavnik) diff --git a/application/sl/configuration.texy b/application/sl/configuration.texy index e05f269f44..2d1c3c3742 100644 --- a/application/sl/configuration.texy +++ b/application/sl/configuration.texy @@ -13,11 +13,15 @@ application: # prikazuje ploščo "Nette Application" v programu Tracy BlueScreen? debugger: ... # (bool) privzeto je true - # ali se ob napaki pokliče error-presenter? - catchExceptions: ... # (bool) privzeto true v produkcijskem načinu + # se bo ob napaki poklical predstavnik napak? + # učinkuje samo v načinu za razvijalce + catchExceptions: ... # (bool) privzeto true # ime programa error-presenter - errorPresenter: Error # (string) privzeto 'Nette:Error' + errorPresenter: Error # (string|array) privzeto 'Nette:Error' + + # določa vzdevke za voditelje in dogodke + aliases: ... # določa pravila za razrešitev imena predstavnika na razred mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) privzeto je false ``` -Ker se v razvojnem načinu predstavniki napak privzeto ne kličejo, napake pa prikazuje Tracy, sprememba vrednosti `catchExceptions` v `true` pomaga preveriti, ali predstavniki napak med razvojem delujejo pravilno. +Od različice `nette/application` 3.2 je mogoče določiti par predstavnikov napak: + +```neon +application: + errorPresenter: + 4xx: Error4xx # za Nette\Application\BadRequestException + 5xx: Error5xx # za druge izjeme +``` Možnost `silentLinks` določa, kako se Nette obnaša v razvojnem načinu, ko generiranje povezav ne uspe (na primer ker ni predstavnika itd.). Privzeta vrednost `false` pomeni, da Nette sproži `E_USER_WARNING`. Nastavitev na `true` to sporočilo o napaki odpravi. V produkcijskem okolju se vedno sproži `E_USER_WARNING`. Na to obnašanje lahko vplivamo tudi z nastavitvijo spremenljivke presenterja [$invalidLinkMode |creating-links#Invalid Links]. - [Prikazovanje določa pravila, |modules#mapping] po katerih se ime razreda izpelje iz imena predstavnika. +[Psevdonimi poenostavijo sklicevanje na |creating-links#aliases] pogosto uporabljene predstavnike. + +[Prikazovanje določa pravila, |directory-structure#Presenter Mapping] po katerih se ime razreda izpelje iz imena predstavnika. Samodejna registracija predavateljev .[#toc-automatic-registration-of-presenters] @@ -82,6 +95,9 @@ latte: # omogoča [preverjanje ustvarjene kode |latte:develop#Checking Generated Code] phpLinter: ... # (string) privzeto je null + # nastavi krajevni jezik + locale: cs_CZ # (string) privzeto je nič + # razred $this->template templateClass: App\MyTemplateClass # privzeto je Nette\Bridges\ApplicationLatte\DefaultTemplate ``` @@ -91,7 +107,7 @@ latte: ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` /--comment diff --git a/application/sl/creating-links.texy b/application/sl/creating-links.texy index 4943feeb98..8f115354f8 100644 --- a/application/sl/creating-links.texy +++ b/application/sl/creating-links.texy @@ -38,7 +38,7 @@ Lahko ji posredujemo tudi poimenovane parametre. Naslednja povezava posreduje pa detail ``` -Če metoda `ProductPresenter::renderShow()` v svojem podpisu nima zapisa `$lang`, lahko vrednost parametra prebere z uporabo `$lang = $this->getParameter('lang')`. +Če metoda `ProductPresenter::renderShow()` v svojem podpisu nima `$lang`, lahko vrednost parametra pridobi z uporabo `$lang = $this->getParameter('lang')` ali iz [lastnosti |presenters#Request Parameters]. Če so parametri shranjeni v polju, jih je mogoče razširiti z operatorjem `...` (ali `(expand)` v Latte 2.x): @@ -103,7 +103,7 @@ Osnovna oblika je torej `Presenter:action`: home ``` -Povezave lahko kažejo tudi na druge [module |modules]. Tu se povezave razlikujejo na relativne na podmodule ali absolutne. Načelo je podobno kot pri diskovnih poteh, le da so namesto poševnic dvopičja. Predpostavimo, da je dejanski predstavnik del modula `Front`, potem bomo zapisali: +Povezave lahko kažejo tudi na druge [module |directory-structure#Presenters and Templates]. Pri tem se povezave razlikujejo na relativne do podmodulov ali absolutne. Načelo je podobno kot pri diskovnih poteh, le da so namesto poševnic dvopičja. Predpostavimo, da je dejanski predstavnik del modula `Front`, potem bomo zapisali: ```latte link to Front:Shop:Product:show @@ -140,7 +140,7 @@ Ciljna stran `this` bo ustvarila povezavo do trenutne strani: refresh ``` -Hkrati se vsi parametri, navedeni v podpisu `render()` ali `action()` se prenesejo. Če smo torej na straneh `Product:show` in `id:123`, bo tudi povezava do strani `this` prenesla ta parameter. +Hkrati se vsi parametri, navedeni v podpisu `action()` ali . `render()` metode, če je `action()` niso opredeljeni, se prenesejo. Če smo torej na straneh `Product:show` in `id:123`, bo povezava do strani `this` prenesla tudi ta parameter. Seveda je mogoče parametre navesti tudi neposredno: @@ -213,7 +213,7 @@ Ker so [komponente |components] ločene enote za večkratno uporabo, ki naj ne b Če se želimo v predlogi komponente povezati s predstavniki, uporabimo oznako `{plink}`: ```latte -home +home ``` ali v kodi @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Vzdevki .[#toc-aliases]{data-version:v3.2.2} +============================================ + +Včasih je koristno, da paru predavatelj:dejanje dodelite lahko zapomljiv vzdevek. Na primer, domačo stran `Front:Home:default` lahko poimenujete preprosto kot `home` ali `Admin:Dashboard:default` kot `admin`. + +Vzdevki so v [konfiguraciji |configuration] opredeljeni pod ključem `application › aliases`: + +```neon +application: + aliases: + home: Front:Home:default + admin: Admin:Dashboard:default + sign: Front:Sign:in +``` + +V povezavah so zapisani s simbolom at, na primer: + +```latte +administration +``` + +Podprte so v vseh metodah, ki delajo s povezavami, na primer `redirect()` in podobno. + + Neveljavne povezave .[#toc-invalid-links] ========================================= @@ -257,6 +281,6 @@ Kako ustvariti povezave z metodo `link()` comfort, vendar brez prisotnosti preds LinkGenerator je storitev, ki jo lahko imate posredovano skozi konstruktor in nato ustvarite povezave z njeno metodo `link()`. -V primerjavi s predstavniki obstaja razlika. LinkGenerator ustvari vse povezave kot absolutne naslove URL. Poleg tega ni "trenutnega predstavnika", zato ni mogoče določiti samo imena akcije `link('default')` ali relativnih poti do [modulov |modules]. +V primerjavi s predavatelji je razlika. LinkGenerator ustvari vse povezave neposredno kot absolutne naslove URL. Prav tako ni "dejanskega predstavnika", zato kot cilj ne morete navesti imena akcije `link('default')` ali navesti relativnih poti do modulov. Neveljavne povezave vedno vrže `Nette\Application\UI\InvalidLinkException`. diff --git a/application/sl/directory-structure.texy b/application/sl/directory-structure.texy new file mode 100644 index 0000000000..be83a5c12b --- /dev/null +++ b/application/sl/directory-structure.texy @@ -0,0 +1,526 @@ +Struktura imenika aplikacije +**************************** + +
    + +Kako zasnovati jasno in razširljivo imeniško strukturo za projekte v okolju Nette Framework? Predstavili vam bomo preizkušene prakse, ki vam bodo pomagale organizirati kodo. Naučili se boste: + +- kako **logično strukturirati** aplikacijo v imenike +- kako zasnovati strukturo, da se bo z rastjo projekta dobro **razširila** +- katere so **možne alternative** in njihove prednosti ali slabosti + +
    + + +Pomembno je omeniti, da samo ogrodje Nette Framework ne vztraja pri nobeni posebni strukturi. Zasnovan je tako, da ga je mogoče zlahka prilagoditi vsem potrebam in željam. + + +Osnovna struktura projekta .[#toc-basic-project-structure] +========================================================== + +Čeprav ogrodje Nette Framework ne narekuje nobene fiksne strukture imenikov, obstaja preverjena privzeta ureditev v obliki [spletnega projekta |https://github.com/nette/web-project]: + +/--pre +web-project/ +├── app/ ← imenik aplikacij +├── assets/ ← SCSS, JS datoteke, slike..., lahko tudi resources/ +├── bin/ ← skripte ukazne vrstice +├── config/ ← konfiguracija +├── log/ ← zabeležene napake +├── temp/ ← začasne datoteke, predpomnilnik +├── tests/ ← testi +├── vendor/ ← knjižnice, ki jih je namestil Composer +└── www/ ← javni imenik (document-root) +\-- + +To strukturo lahko poljubno spreminjate glede na svoje potrebe - preimenujete ali premikate mape. Nato morate le prilagoditi relativne poti do imenikov v `Bootstrap.php` in po možnosti `composer.json`. Nič drugega ni potrebno, nobene zapletene ponovne konfiguracije, nobenih stalnih sprememb. Nette ima pametno samodejno zaznavanje in samodejno prepozna lokacijo aplikacije, vključno z njeno bazo URL. + + +Načela organizacije kode .[#toc-code-organization-principles] +============================================================= + +Ko prvič raziskujete nov projekt, se morate znati hitro orientirati. Predstavljajte si, da kliknete na imenik `app/Model/` in vidite to strukturo: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +Iz nje boste izvedeli le, da projekt uporablja nekatere storitve, skladišča in entitete. Ne boste izvedeli ničesar o dejanskem namenu aplikacije. + +Oglejmo si drugačen pristop - **organizacija po domenah**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Na prvi pogled je jasno, da gre za spletno mesto e-trgovine. Že imena imenikov razkrivajo, kaj aplikacija zmore - dela s plačili, naročili in izdelki. + +Prvi pristop (organizacija po vrsti razreda) v praksi prinaša več težav: koda, ki je logično povezana, je razpršena po različnih mapah in med njimi je treba preskakovati. Zato se bomo organizirali po domenah. + + +Prostori imen .[#toc-namespaces] +-------------------------------- + +Običajno struktura imenikov 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`, mora imeti imenski prostor `App\Model\Product`. To načelo pomaga pri usmerjanju kode in poenostavlja samodejno nalaganje. + + +Ednina in množina v imenih .[#toc-singular-vs-plural-in-names] +-------------------------------------------------------------- + +Opazite, da za glavne imenike aplikacij uporabljamo ednino: `app` `config` , `log`, `temp`, `www`. Enako velja tudi znotraj aplikacije: `Model`, `Core`, `Presentation`. To je zato, ker vsak od njih predstavlja en enoten koncept. + +Podobno tudi `app/Model/Product` predstavlja vse o izdelkih. Ne imenujemo je `Products`, ker to ni mapa, polna izdelkov (ki bi vsebovala datoteke, kot so `iphone.php`, `samsung.php`). To je imenski prostor, ki vsebuje razrede za delo z izdelki - `ProductRepository.php`, `ProductService.php`. + +Mapa `app/Tasks` je množinska, ker vsebuje nabor samostojnih izvršilnih skript - `CleanupTask.php`, `ImportTask.php`. Vsaka od njih je samostojna enota. + +Zaradi doslednosti priporočamo uporabo: +- ednino za imenske prostore, ki predstavljajo funkcionalno enoto (tudi če delate z več enotami) +- množino za zbirke neodvisnih enot +- V primeru negotovosti ali če o tem ne želite razmišljati, izberite ednino + + +Javni imenik `www/` .[#toc-public-directory-www] +================================================ + +Ta imenik je edini, ki je dostopen s spleta (tako imenovani koren dokumentov). Pogosto lahko namesto imena `www/` naletite na ime `public/` - to je le stvar konvencije in ne vpliva na funkcionalnost. Imenik vsebuje: +- [vstopno točko |bootstrap#index.php] aplikacije `index.php` +- datoteko `.htaccess` s pravili mod_rewrite (za Apache) +- statične datoteke (CSS, JavaScript, slike) +- naložene datoteke + +Za ustrezno varnost aplikacije je ključnega pomena, da je pravilno [konfiguriran document-root |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url]. + +.[note] +V ta imenik nikoli ne postavite mape `node_modules/` - vsebuje na tisoče datotek, ki so lahko izvedljive in ne smejo biti javno dostopne. + + +Imenik aplikacij `app/` .[#toc-application-directory-app] +========================================================= + +To je glavni imenik z aplikacijsko kodo. Osnovna struktura: + +/--pre +app/ +├── Core/ ← infrastrukturne zadeve +├── Model/ ← poslovna logika +├── Presentation/ ← predstavitve in predloge +├── Tasks/ ← skripte ukazov +└── Bootstrap.php ← zagonski razred aplikacije +\-- + +`Bootstrap.php` je [zagonski razred aplikacije, |bootstrap] ki inicializira okolje, naloži konfiguracijo in ustvari vsebnik DI. + +Zdaj si podrobno oglejmo posamezne podimenike. + + +Predstavniki in predloge .[#toc-presenters-and-templates] +========================================================= + +Predstavitveni del aplikacije imamo v imeniku `app/Presentation`. Druga možnost je kratek naslov `app/UI`. To je prostor za vse predstavitve, njihove predloge in morebitne pomožne razrede. + +To plast organiziramo po domenah. V kompleksnem projektu, ki združuje e-trgovino, blog in API, bi bila struktura videti takole: + +/--pre +app/Presentation/ +├── Shop/ ← e-trgovina frontend +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← blog +│ ├── Home/ +│ └── Post/ +├── Admin/ ← uprava +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← Končne točke API + └── V1/ +\-- + +Za preprost blog pa bi uporabili to strukturo: + +/--pre +app/Presentation/ +├── Front/ ← spletna stran frontend +│ ├── Home/ +│ └── Post/ +├── Admin/ ← uprava +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, zemljevide itd. +\-- + +V mapah, kot sta `Home/` ali `Dashboard/`, so predstavniki in predloge. Mape, kot so `Front/`, `Admin/` ali `Api/`, se imenujejo **moduli**. Tehnično gledano so to običajni imeniki, ki služijo za logično organizacijo aplikacije. + +Vsaka mapa s predstavnikom vsebuje podobno poimenovane predstavnike in njihove predloge. Na primer, mapa `Dashboard/` vsebuje: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← voditelj +└── default.latte ← predloga +\-- + +Ta struktura imenikov se odraža v imenskih prostorih razredov. Na primer, `DashboardPresenter` je v imenskem prostoru `App\Presentation\Admin\Dashboard` (glejte [preslikavo predavateljev |#presenter mapping]): + +```php +namespace App\Presentation\Admin\Dashboard; + +class DashboardPresenter extends Nette\Application\UI\Presenter +{ + //... +} +``` + +Predstavnik `Dashboard` znotraj modula `Admin` v aplikaciji označujemo z zapisom v dvopičju kot `Admin:Dashboard`. Na njegovo akcijo `default` pa nato kot `Admin:Dashboard:default`. Za vgnezdene module uporabljamo več dvopičij, na primer `Shop:Order:Detail:default`. + + +Razvoj prilagodljive strukture .[#toc-flexible-structure-development] +--------------------------------------------------------------------- + +Ena od velikih prednosti te strukture je, kako elegantno se prilagaja naraščajočim potrebam projekta. Kot primer vzemimo del, ki ustvarja vire XML. Na začetku imamo preprost obrazec: + +/--pre +Export/ +├── ExportPresenter.php ← en predavatelj za ves izvoz +├── sitemap.latte ← predlogo za karto spletnega mesta +└── feed.latte ← predlogo za vir RSS +\-- + +Sčasoma se doda več vrst virov in zanje potrebujemo več logike... Ni problema! Mapa `Export/` preprosto postane modul: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← krma za Amazon + └── ebay.latte ← vir za eBay +\-- + +Preoblikovanje je popolnoma nemoteno - ustvarite nove podmape, razdelite kodo vanje in posodobite povezave (npr. iz `Export:feed` v `Export:Feed:amazon`). Zaradi tega lahko strukturo postopoma širimo po potrebi, raven gnezdenja ni v ničemer omejena. + +Če imate na primer v administraciji veliko predstavnikov, povezanih z upravljanjem naročil, kot so `OrderDetail`, `OrderEdit`, `OrderDispatch` itd, lahko za boljšo organizacijo ustvarite modul (mapo) `Order`, ki bo vseboval (mape za) predstavnike `Detail`, `Edit`, `Dispatch` in druge. + + +Lokacija predloge .[#toc-template-location] +------------------------------------------- + +V prejšnjih primerih smo videli, da se predloge nahajajo neposredno v mapi s predstavitvijo: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← voditelj +├── DashboardTemplate.php ← neobvezen razred predloge +└── default.latte ← Predloga +\-- + +Ta lokacija se v praksi izkaže za najprimernejšo - vse povezane datoteke imate takoj pri roki. + +Predloge lahko namestite tudi v podmapo `templates/`. Nette podpira obe različici. Predloge lahko postavite tudi povsem zunaj mape `Presentation/`. Vse o možnostih lokacije predlog najdete v poglavju [Iskanje predlog |templates#Template Lookup]. + + +Pomožni razredi in komponente .[#toc-helper-classes-and-components] +------------------------------------------------------------------- + +Predstavniki in predloge so pogosto opremljeni z drugimi pomožnimi datotekami. Razporedimo jih logično glede na njihovo področje uporabe: + +1. **Neposredno s predstavitvijo** v primeru posebnih komponent za določeno predstavitev: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← komponenta za uvrstitev izdelka na seznam +└── FilterForm.php ← obrazec za filtriranje +\-- + +2. **Za modul** - priporočamo uporabo mape `Accessory`, ki je lepo postavljena na začetek 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/ +\-- + +Lahko pa pomožne razrede, kot sta `LatteExtension.php` ali `TemplateFilters.php`, namestite v infrastrukturno mapo `app/Core/Latte/`. Komponente pa v mapo `app/Components`. Izbira je odvisna od skupinskih konvencij. + + +Model - srce aplikacije .[#toc-model-heart-of-the-application] +============================================================== + +Model vsebuje vso poslovno logiko aplikacije. Za njegovo organizacijo velja enako pravilo - strukturiramo ga po domenah: + +/--pre +app/Model/ +├── Payment/ ← vse o plačilih +│ ├── PaymentFacade.php ← glavna vstopna točka +│ ├── PaymentRepository.php +│ ├── Payment.php ← entiteta +├── Order/ ← vse o naročilih +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← vse o odpremi +\-- + +V modelu se običajno srečamo s temi vrstami razredov: + +**Fasade**: predstavljajo glavno vstopno točko v določeno domeno v aplikaciji. Delujejo kot orkestrator, ki usklajuje sodelovanje med različnimi storitvami za izvajanje celotnih primerov uporabe (kot sta "ustvariti naročilo" ali "obdelati plačilo"). Pod orkestracijsko plastjo fasada skriva podrobnosti izvajanja pred preostalim delom aplikacije in tako zagotavlja čist vmesnik za delo z določeno domeno. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // potrjevanje + // ustvarjanje naročila. + // pošiljanje e-pošte + // pisanje v statistiko + } +} +``` + +**Službe**: osredotočajo se na posebne poslovne operacije znotraj domene. Za razliko od fasad, ki orkestrirajo celotne primere uporabe, storitev izvaja specifično poslovno logiko (kot so izračuni cen ali obdelava plačil). Storitve so običajno brez stanja in jih lahko uporabljajo fasade kot gradnike za kompleksnejše operacije ali drugi deli aplikacije neposredno za enostavnejša opravila. + +```php +class PricingService +{ + public function calculateTotal(Order $order): Money + { + // izračun cene + } +} +``` + +**Hrambe**: skrbijo za vso komunikacijo s hrambo podatkov, običajno s podatkovno zbirko. Njihova naloga je nalaganje in shranjevanje entitet ter izvajanje metod za njihovo iskanje. Skladišče ščiti preostalo aplikacijo pred podrobnostmi izvajanja podatkovne zbirke 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 s časom spreminjajo. Običajno so to razredi, ki so s pomočjo ORM (kot sta Nette Database Explorer ali Doctrine) preslikani v tabele podatkovne zbirke. Entitete lahko vsebujejo poslovna pravila v zvezi z njihovimi podatki in logiko potrjevanja. + +```php +// Entiteta, preslikana v tabelo naročil v zbirki podatkov +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 znesek denarja ali e-poštni naslov. Dva primerka objekta vrednosti z enakimi vrednostmi veljata za enaka. + + +Koda infrastrukture .[#toc-infrastructure-code] +=============================================== + +V mapi `Core/` (ali tudi `Infrastructure/`) se nahaja tehnični temelj aplikacije. Infrastrukturna koda običajno vključuje: + +/--pre +app/Core/ +├── Router/ ← usmerjanje in upravljanje URL. +│ └── RouterFactory.php +├── Security/ ← avtentikacija in avtorizacija. +│ ├── Authenticator.php +│ └── Authorizator.php +├── Logging/ ← beleženje in spremljanje +│ ├── SentryLogger.php +│ └── FileLogger.php +├── Cache/ ← plast predpomnilnika +│ └── FullPageCache.php +└── Integration/ ← integracija z zunanjimi storitvami + ├── Slack/ + └── Stripe/ +\-- + +Za manjše projekte seveda zadostuje ravna struktura: + +/--pre +Core/ +├── RouterFactory.php +├── Authenticator.php +└── QueueMailer.php +\-- + +To je koda, ki: + +- Obravnava tehnično infrastrukturo (usmerjanje, beleženje, predpomnilnik). +- vključuje zunanje storitve (Sentry, Elasticsearch, Redis) +- zagotavlja osnovne storitve za celotno aplikacijo (pošta, zbirka podatkov) +- je večinoma neodvisna od določene domene - predpomnilnik ali logger deluje enako za e-trgovino ali blog. + +Se sprašujete, ali določen razred spada sem ali v model? Ključna razlika je v tem, da je koda v `Core/`: + +- ne ve ničesar o domeni (izdelki, naročila, članki) +- Običajno jo je mogoče prenesti v drug projekt +- rešuje "kako deluje" (kako poslati pošto) in ne "kaj počne" (kakšno pošto poslati) + +Primer za boljše razumevanje: + +- `App\Core\MailerFactory` - ustvari primerke razreda za pošiljanje e-pošte, skrbi za nastavitve SMTP +- `App\Model\OrderMailer` - uporablja `MailerFactory` za pošiljanje e-poštnih sporočil o naročilih, pozna njihove predloge in ve, kdaj jih je treba poslati + + +Skripte ukazov .[#toc-command-scripts] +====================================== + +Aplikacije morajo pogosto opravljati naloge zunaj običajnih zahtevkov HTTP - bodisi gre za obdelavo podatkov v ozadju, vzdrževanje ali periodična opravila. Za izvajanje se uporabljajo preproste skripte v imeniku `bin/`, medtem ko je dejanska izvedbena logika nameščena v imeniku `app/Tasks/` (ali `app/Commands/`). + +Primer: + +/--pre +app/Tasks/ +├── Maintenance/ ← skripte za vzdrževanje +│ ├── CleanupCommand.php ← brisanje starih podatkov +│ └── DbOptimizeCommand.php ← optimizacija podatkovne zbirke +├── Integration/ ← integracija z zunanjimi sistemi +│ ├── ImportProducts.php ← uvoz iz sistema dobavitelja +│ └── SyncOrders.php ← sinhronizacija naročil +└── Scheduled/ ← redna opravila + ├── NewsletterCommand.php ← pošiljanje novic + └── ReminderCommand.php ← obveščanje strank +\-- + +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čih e-poštnih sporočil pa spada v `Tasks/`. + +Opravila se običajno [izvajajo iz ukazne vrstice |https://blog.nette.org/en/cli-scripts-in-nette-application] ali prek programa cron. Lahko se zaženejo tudi prek zahteve HTTP, vendar je pri tem treba upoštevati varnost. Predstavnik, ki izvaja opravilo, mora biti zavarovan, na primer samo za prijavljene uporabnike ali z močnim žetonom in dostopom z dovoljenih naslovov IP. Za dolga opravila je treba povečati časovno omejitev skripta in uporabiti spletno stran `session_write_close()`, da se prepreči zaklepanje seje. + + +Drugi možni imeniki .[#toc-other-possible-directories] +====================================================== + +Poleg omenjenih osnovnih imenikov lahko glede na potrebe projekta dodate tudi druge specializirane mape. Oglejmo si najpogostejše in njihovo uporabo: + +/--pre +app/ +├── Api/ ← Logika API je neodvisna od predstavitvene plasti +├── Database/ ← migracijske skripte in sejalnike za testne podatke. +├── Components/ ← skupne vizualne komponente v aplikaciji +├── Event/ ← uporabno, če uporabljate arhitekturo, ki temelji na dogodkih +├── Mail/ ← e-poštne predloge in povezana logika +└── Utils/ ← pomožni razredi +\-- + +Za skupne vizualne komponente, ki se uporabljajo v predstavitvah v celotni aplikaciji, lahko uporabite mapo `app/Components` ali `app/Controls`: + +/--pre +app/Components/ +├── Form/ ← komponente obrazca v skupni rabi +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← komponente za izpise podatkov +│ └── DataGrid.php +└── Navigation/ ← navigacijski elementi + ├── Breadcrumbs.php + └── Menu.php +\-- + +V to mapo spadajo komponente z bolj zapleteno logiko. Če želite komponente deliti med več projekti, jih je dobro ločiti v samostojni paket Composer. + +V imenik `app/Mail` lahko umestite upravljanje komunikacije z elektronsko pošto: + +/--pre +app/Mail/ +├── templates/ ← e-poštne predloge +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Predavatelj Mapiranje .[#toc-presenter-mapping] +=============================================== + +Mapiranje določa pravila za izpeljavo imen razredov iz imen predvajalnikov. Določimo jih v [konfiguraciji |configuration] pod ključem `application › mapping`. + +Na tej strani smo pokazali, da predstavnike namestimo v mapo `app/Presentation` (ali `app/UI`). V konfiguracijski datoteki moramo Nette obvestiti o tej konvenciji. Zadostuje ena vrstica: + +```neon +application: + mapping: App\Presentation\*\**Presenter +``` + +Kako deluje preslikava? Za boljše razumevanje si najprej predstavljajmo aplikacijo brez modulov. Želimo, da razredi predstavnikov spadajo v imenski prostor `App\Presentation`, tako da se predstavnik `Home` preslika v razred `App\Presentation\HomePresenter`. To dosežemo s to konfiguracijo: + +```neon +application: + mapping: App\Presentation\*Presenter +``` + +Mapiranje poteka tako, da se zvezdica v maski `App\Presentation\*Presenter` zamenja z imenom predstavnika `Home`, s čimer dobimo končno ime razreda `App\Presentation\HomePresenter`. Enostavno! + +Vendar, kot vidite v primerih v tem in drugih poglavjih, umeščamo predstavitvene razrede v istoimenske podimenike, na primer predstavitveni razred `Home` se preslika v razred `App\Presentation\Home\HomePresenter`. To dosežemo s podvojitvijo dvopičja (zahteva aplikacijo Nette 3.2): + +```neon +application: + mapping: App\Presentation\**Presenter +``` + +Zdaj bomo prešli na preslikavo predstavnikov v module. Za vsak modul lahko določimo posebno kartiranje: + +```neon +application: + mapping: + Front: App\Presentation\Front\**Presenter + Admin: App\Presentation\Admin\**Presenter + Api: App\Api\*Presenter +``` + +V skladu s to konfiguracijo je predstavnik `Front:Home` preslikan v razred `App\Presentation\Front\Home\HomePresenter`, medtem ko je predstavnik `Api:OAuth` preslikan v razred `App\Api\OAuthPresenter`. + +Ker imata modula `Front` in `Admin` podoben način preslikave in ker bo takih modulov verjetno več, je mogoče ustvariti splošno pravilo, ki jih bo nadomestilo. V masko razreda bo dodana nova zvezdica za modul: + +```neon +application: + mapping: + *: App\Presentation\*\**Presenter + Api: App\Api\*Presenter +``` + +Deluje tudi za globlje ugnezdene imeniške strukture, kot je predstavnik `Admin:User:Edit`, kjer se segment z zvezdico ponovi za vsako raven in ima za rezultat razred `App\Presentation\Admin\User\Edit\EditPresenter`. + +Alternativni zapis je, da namesto niza uporabimo polje, sestavljeno iz treh segmentov. Ta zapis je enakovreden 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 index 9139f15de5..bc390a31ff 100644 --- a/application/sl/how-it-works.texy +++ b/application/sl/how-it-works.texy @@ -22,18 +22,18 @@ Struktura imenikov je videti nekako takole: /--pre web-project/ ├── app/ ← directory with application -│ ├── Presenters/ ← presenter classes -│ │ ├── HomePresenter.php ← Home presenter class -│ │ └── templates/ ← templates directory -│ │ ├── @layout.latte ← template of shared layout -│ │ └── Home/ ← templates for Home presenter -│ │ └── default.latte ← template for action `default` -│ ├── Router/ ← configuration of URL addresses +│ ├── Core/ ← osnovni potrebni razredi +│ │ └── RouterFactory.php ← konfiguracija naslovov URL +│ ├── Presentation/ ← predstavitev, predloge in podobno. +│ │ ├── @layout.latte ← predloga skupne postavitve +│ │ └── Home/ ← Domači imenik predstavnikov +│ │ ├── HomePresenter.php ← Razred HomePresenter +│ │ └── default.latte ← predloga za akcijo default │ └── Bootstrap.php ← booting class Bootstrap ├── bin/ ← scripts for the command line ├── config/ ← configuration files │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← error logs ├── temp/ ← temporary files, cache, … ├── vendor/ ← libraries installed by Composer @@ -45,9 +45,9 @@ Struktura imenikov je videti nekako takole: └── .htaccess ← prohibits access to all directories except www \-- -Lahko kakor koli spremenite imeniško strukturo, preimenujete ali premaknete mape, nato pa samo uredite poti do `log/` in `temp/` v datoteki `Bootstrap.php` ter pot do te datoteke v `composer.json` v razdelku `autoload`. Nič več, nobene zapletene ponovne konfiguracije, nobenih stalnih sprememb. Nette ima [pametno samodejno zaznavanje |bootstrap#development-vs-production-mode]. +Strukturo imenikov lahko spreminjate po želji, preimenujete ali premikate mape - popolnoma prilagodljiva je. Nette ima tudi pametno samodejno zaznavanje in samodejno prepozna lokacijo aplikacije, vključno z njeno bazo URL. -Za nekoliko večje aplikacije lahko mape s predvajalniki in predlogami razdelimo v podimenike (na disku) in v imenske prostore (v kodi), ki jih imenujemo [moduli |modules]. +Pri nekoliko večjih aplikacijah lahko mape predstavnikov in predlog organiziramo v [podimenike |directory-structure#Presenters and templates], razrede pa združimo v imenske prostore, ki jih imenujemo moduli. Imenik `www/` je javni imenik ali dokumentni koren projekta. Lahko ga preimenujete, ne da bi vam bilo treba na strani aplikacije nastaviti kar koli drugega. [Gostovanje |nette:troubleshooting#How to change or remove www directory from URL] morate le [konfigurirati |nette:troubleshooting#How to change or remove www directory from URL] tako, da bo korenina dokumentov prešla v ta imenik. @@ -75,7 +75,7 @@ Njegova naloga je: Katere vrste je tovarna? Ne proizvajamo traktorjev, temveč spletne strani! Počakajte, takoj vam bo razloženo. -Z "inicializacijo okolja" mislimo na primer na to, da se aktivira [Tracy, |tracy:] ki je izjemno orodje za beleženje ali vizualizacijo napak. Beleži napake v produkcijskem strežniku in jih prikazuje neposredno v razvojnem strežniku. Zato je treba pri inicializaciji odločiti tudi, ali spletno mesto deluje v produkcijskem ali razvojnem načinu. Za to Nette uporablja samodejno zaznavanje: če zaženete spletno mesto na lokalnem gostitelju, se zažene v razvijalskem načinu. Ničesar vam ni treba konfigurirati in aplikacija je pripravljena tako za razvojno kot produkcijsko namestitev. Ti koraki so izvedeni in podrobno opisani v poglavju o [razredu Bootstrap |bootstrap]. +Z "inicializacijo okolja" mislimo na primer na aktiviranje programa [Tracy |tracy:], ki je odlično orodje za beleženje in vizualizacijo napak. Na produkcijskih strežnikih beleži napake, na razvojnih pa jih neposredno prikazuje. Zato inicializacija vključuje določitev, ali spletno mesto deluje v produkcijskem ali razvojnem načinu. Za to Nette uporablja [pametno samodejno zaznavanje |bootstrap#development-vs-production-mode]: če spletno mesto zaženete na lokalnem gostitelju, deluje v razvojnem načinu. Konfiguracija ni potrebna in aplikacija je pripravljena tako za razvojno kot produkcijsko namestitev. Ti koraki so izvedeni in podrobno opisani v poglavju [Razred Bootstrap |bootstrap]. Tretja točka (da, drugo smo preskočili, vendar se bomo k njej vrnili) je zagon aplikacije. Obravnavo zahtevkov HTTP v Nette izvaja razred `Nette\Application\Application` (v nadaljevanju `Application`), zato ko rečemo "zagnati aplikacijo", imamo v mislih klic metode z imenom `run()` na objektu tega razreda. @@ -91,7 +91,7 @@ Aplikacije, napisane v okolju Nette, so razdeljene na številne tako imenovane p Aplikacija se začne tako, da zahteva od tako imenovanega usmerjevalnika, da se odloči, kateremu od predstavnikov bo posredoval trenutno zahtevo v obdelavo. Usmerjevalnik se odloči, čigava je to odgovornost. Pregleda vhodni naslov URL `https://example.com/product/123`, ki želi `show` izdelek z `id: 123` kot dejanjem. Dobra navada je, da se pari predstavnik + akcija, ločeni z dvopičjem, zapišejo kot `Product:show`. -Usmerjevalnik je torej pretvoril naslov URL v par `Presenter:action` + parametri, v našem primeru `Product:show` + `id: 123`. Kako je videti usmerjevalnik, si lahko ogledate v datoteki `app/Router/RouterFactory.php`, podrobno pa ga bomo opisali v poglavju [Usmerjanje |Routing]. +Usmerjevalnik je torej pretvoril naslov URL v par `Presenter:action` + parametri, v našem primeru `Product:show` + `id: 123`. Kako je videti usmerjevalnik, si lahko ogledate v datoteki `app/Core/RouterFactory.php`, podrobno pa ga bomo opisali v poglavju [Usmerjanje |Routing]. Pojdimo naprej. Aplikacija že pozna ime predavatelja in lahko nadaljuje. Z ustvarjanjem predmeta `ProductPresenter`, ki je koda predstavnika `Product`. Natančneje, za ustvarjanje predstavnika zaprosi vsebnik DI, saj je izdelovanje objektov njegova naloga. @@ -121,12 +121,9 @@ Tako je bila klicana metoda `renderShow(123)`, katere koda je izmišljen primer, Nato predstavnik vrne odgovor. To je lahko stran HTML, slika, dokument XML, pošiljanje datoteke z diska, JSON ali preusmeritev na drugo stran. Pomembno je, da če izrecno ne navedemo, kako odgovoriti (kar je primer `ProductPresenter`), bo odgovor prikaz predloge s stranjo HTML. Zakaj? No, ker v 99 % primerov želimo izrisati predlogo, zato predstavnik to vedenje sprejme kot privzeto in nam želi olajšati delo. To je Nettejeva poanta. -Ni nam treba niti navesti, katero predlogo želimo narisati, on pot do nje izpelje po preprosti logiki. V primeru predstavnika `Product` in akcije `show`, poskuša preveriti, ali obstaja ena od teh datotek s predlogami glede na imenik, v katerem se nahaja razred `ProductPresenter`: +Sploh nam ni treba navesti, katero predlogo je treba prikazati; ogrodje bo pot določilo samo. V primeru akcije `show` preprosto poskuša naložiti predlogo `show.latte` v imeniku z razredom `ProductPresenter`. Prav tako poskuša najti postavitev v datoteki `@layout.latte` (več o [iskanju predlog |templates#Template Lookup]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -Prav tako poskuša poiskati postavitev v datoteki `@layout.latte` in nato upodobi predlogo. Zdaj je naloga predstavnika in celotne aplikacije končana. Če predloga ne obstaja, se vrne stran z napako 404. Več o predstavitvah si lahko preberete na strani [Predstavitve |Presenters]. +Nato se predloge izrišejo. S tem je naloga predstavnika in celotne aplikacije končana in delo je opravljeno. Če predloga ne bi obstajala, bi se vrnila stran z napako 404. Več o predstavnikih si lahko preberete na strani [Predstavniki |presenters]. [* request-flow.svg *] @@ -137,7 +134,7 @@ Da bi se prepričali, poskusite celoten postopek ponoviti z nekoliko drugačnim 3) usmerjevalnik dekodira naslov URL kot par `Home:default` 4) ustvari se objekt `HomePresenter` 5) kličemo metodo `renderDefault()` (če obstaja) -6) prikaže se predloga `templates/Home/default.latte` z razporeditvijo `templates/@layout.latte` +6) prikaže se predloga `default.latte` z razporeditvijo `@layout.latte` Morda ste zdaj naleteli na veliko novih konceptov, vendar verjamemo, da so smiselni. Ustvarjanje aplikacij v programu Nette je zelo enostavno. diff --git a/application/sl/modules.texy b/application/sl/modules.texy deleted file mode 100644 index d37ea342b1..0000000000 --- a/application/sl/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Moduli -****** - -.[perex] -V Nette moduli predstavljajo logične enote, ki sestavljajo aplikacijo. Vključujejo predstavnike, predloge, lahko tudi komponente in razrede modelov. - -En imenik za predstavnike in en imenik za predloge za prave projekte ne bi bil dovolj. Če je v eni mapi na desetine datotek, je to vsaj neorganizirano. Kako to odpraviti? Preprosto jih razdelimo v podimenike na disku in v imenske prostore v kodi. In točno to naredijo moduli Nette. - -Pozabimo torej na eno mapo za predstavnike in predloge in namesto tega ustvarimo module, na primer `Admin` in `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← directory with modules -│ ├── Admin/ ← module Admin -│ │ ├── Presenters/ ← its presenters -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← module Front -│ └── Presenters/ ← its presenters -│ └── ... -\-- - -Ta struktura imenikov se bo odražala v imenskih prostorih razredov, tako da bo na primer `DashboardPresenter` v imenskem prostoru `App\Modules\Admin\Presenters`: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Na predvajalnik `Dashboard` znotraj modula `Admin` se v aplikaciji sklicujemo z uporabo zapisa v dvopičju kot na `Admin:Dashboard`, na njegovo akcijo `default` pa kot na `Admin:Dashboard:default`. -In kako Nette pravilno ve, da `Admin:Dashboard` predstavlja razred `App\Modules\Admin\Presenters\DashboardPresenter`? To je določeno s [preslikavo |#mapping] v konfiguraciji. -Podana struktura torej ni trdno določena in jo lahko spreminjate glede na svoje potrebe. - -Moduli seveda lahko poleg predstavnikov in predlog vsebujejo tudi vse druge elemente, kot so komponente, razredi modelov itd. - - -Vgnezdeni moduli .[#toc-nested-modules] ---------------------------------------- - -Ni nujno, da moduli tvorijo le ravno strukturo, ustvarite lahko tudi podmodule, na primer: - -/--pre -app/ -├── Modules/ ← directory with modules -│ ├── Blog/ ← module Blog -│ │ ├── Admin/ ← submodule Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← submodule Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← module Forum -│ │ └── ... -\-- - -Tako je modul `Blog` razdeljen na podmodula `Admin` in `Front`. Tudi to se bo odražalo v imenskih prostorih, ki bodo `App\Modules\Blog\Admin\Presenters` itd. Predstavnik `Dashboard` znotraj podmodula se imenuje `Blog:Admin:Dashboard`. - -Gnezdenje je lahko tako globoko, kot želite, zato lahko ustvarite podmodule. - - -Ustvarjanje povezav .[#toc-creating-links] ------------------------------------------- - -Povezave v predlogah za predstavitev so relativne glede na trenutni modul. Tako povezava `Foo:default` vodi do predstavitvene predloge `Foo` v istem modulu kot trenutna predstavitvena predloga. Če je trenutni modul na primer `Front`, potem povezava poteka takole: - -```latte -link to Front:Product:show -``` - -Povezava je relativna tudi, če vključuje ime modula, ki se potem šteje za podmodul: - -```latte -link to Front:Shop:Product:show -``` - -Absolutne povezave se zapišejo podobno kot absolutne poti na disku, vendar s podpičjem namesto šumnikov. Tako se absolutna povezava začne z dvopičjem: - -```latte -link to Admin:Product:show -``` - -Če želimo ugotoviti, ali smo v določenem modulu ali njegovem podmodulu, lahko uporabimo funkcijo `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Usmerjanje .[#toc-routing] --------------------------- - -Glejte [poglavje o usmerjanju |routing#Modules]. - - -Kartiranje .[#toc-mapping] --------------------------- - -Določa pravila, po katerih se ime razreda izpelje iz imena predstavnika. Zapišemo jih v [konfiguracijo |configuration] pod ključ `application › mapping`. - -Začnimo z vzorcem, ki ne uporablja modulov. Želeli bomo le, da imajo razredi predstavnikov imenski prostor `App\Presenters`. To pomeni, da mora biti predstavnik, kot je `Home`, preslikan v razred `App\Presenters\HomePresenter`. To lahko dosežemo z naslednjo konfiguracijo: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -V maski razreda se ime predvajalnika nadomesti z zvezdico, rezultat pa je ime razreda. Enostavno! - -Če voditelje razdelimo na module, lahko za vsak modul pripravimo lastno preslikavo: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Sedaj je predstavnik `Front:Home` preslikan v razred `App\Modules\Front\Presenters\HomePresenter`, predstavnik `Admin:Dashboard` pa v razred `App\Modules\Admin\Presenters\DashboardPresenter`. - -Bolj praktično je ustvariti splošno (zvezdno) pravilo, ki bo nadomestilo prvi dve. Dodatna zvezdica bo dodana maski razreda samo za ta modul: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Kaj pa, če uporabljamo vgnezdene module in imamo predvajalnik `Admin:User:Edit`? V tem primeru se segment z zvezdico, ki predstavlja modul za vsako raven, preprosto ponovi in rezultat je razred `App\Modules\Admin\User\Presenters\EditPresenter`. - -Alternativni zapis je, da namesto niza uporabimo polje, sestavljeno iz treh segmentov. Ta zapis je enakovreden prejšnjemu: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -Privzeta vrednost je `*: *Module\*Presenter`. diff --git a/application/sl/presenters.texy b/application/sl/presenters.texy index abfd2f2527..1f1e6b993b 100644 --- a/application/sl/presenters.texy +++ b/application/sl/presenters.texy @@ -60,7 +60,7 @@ Podobno kot pri metodi `render()`. Medtem ko `render()` je namenjena Pomembno je, da `action()` se pokliče pred `render()`, tako da lahko znotraj njega morebiti spremenimo naslednji potek življenjskega cikla, tj. spremenimo predlogo, ki se bo izrisala, in tudi metodo `render()` ki bo poklicana, z uporabo `setView('otherView')`. -Parametri iz zahteve se posredujejo metodi. Za parametre je mogoče in priporočljivo določiti tipe, npr. `actionShow(int $id, string $slug = null)` - če parameter `id` manjka ali če ni celo število, predstavnik vrne [napako 404 |#Error 404 etc.] in zaključi operacijo. +Parametri iz zahteve se posredujejo metodi. Za parametre je mogoče in priporočljivo določiti tipe, npr. `actionShow(int $id, ?string $slug = null)` - če parameter `id` manjka ali če ni celo število, predstavnik vrne [napako 404 |#Error 404 etc.] in zaključi operacijo. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ V predlogi so ta sporočila na voljo v spremenljivki `$flashes` kot objekti `std Napaka 404 itd. .[#toc-error-404-etc] ===================================== -Kadar ne moremo izpolniti zahteve, ker na primer članek, ki ga želimo prikazati, ne obstaja v zbirki podatkov, bomo z metodo `error(string $message = null, int $httpCode = 404)`, ki predstavlja napako HTTP 404, vrgli napako 404: +Kadar ne moremo izpolniti zahteve, ker na primer članek, ki ga želimo prikazati, ne obstaja v zbirki podatkov, bomo z metodo `error(?string $message = null, int $httpCode = 404)`, ki predstavlja napako HTTP 404, vrgli napako 404: ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Parametri zahtevka .[#toc-request-parameters] +============================================= + +Predstavitelj in vsaka komponenta pridobi svoje parametre iz zahteve HTTP. Njihove vrednosti lahko pridobite z metodo `getParameter($name)` ali `getParameters()`. Vrednosti so nizi ali nizi nizov, v bistvu surovi podatki, pridobljeni neposredno iz URL. + +Zaradi večjega udobja priporočamo, da so parametri dostopni prek lastnosti. Preprosto jih opišite z ukazom `#[Parameter]` atributom: + +```php +use Nette\Application\Attributes\Parameter; // ta vrstica je pomembna + +class HomePresenter extends Nette\Application\UI\Presenter +{ + #[Parameter] + public string $theme; // mora biti javna. +} +``` + +Za lastnosti predlagamo, da navedete vrsto podatkov (npr. `string`). Nette bo na podlagi tega samodejno določil vrednost. Vrednosti parametrov je mogoče tudi [potrditi |#Validation of Parameters]. + +Pri ustvarjanju povezave lahko neposredno določite vrednost za parametre: + +```latte +click +``` + + Trajni parametri .[#toc-persistent-parameters] ============================================== @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Če ima `$this->lang` vrednost, kot je `'en'`, bodo povezave, ustvarjene z uporabo `link()` ali `n:href`, vsebovale tudi parameter `lang=en`. In ko boste povezavo kliknili, bo ta spet imela vrednost `$this->lang = 'en'`. -Za lastnosti priporočamo, da vključite podatkovno vrsto (npr. `string`), vključite pa lahko tudi privzeto vrednost. Vrednosti parametrov je mogoče [potrditi |#Validation of Persistent Parameters]. +Za lastnosti priporočamo, da vključite podatkovno vrsto (npr. `string`), vključite pa lahko tudi privzeto vrednost. Vrednosti parametrov je mogoče [potrditi |#Validation of Parameters]. Trajni parametri se privzeto posredujejo med vsemi dejanji določenega predstavnika. Če jih želite posredovati med več predstavniki, jih morate opredeliti: @@ -307,18 +333,12 @@ Poglobitev .[#toc-going-deeper] To, kar smo doslej prikazali v tem poglavju, bo verjetno zadostovalo. Naslednje vrstice so namenjene tistim, ki jih predstavniki zanimajo poglobljeno in želijo vedeti vse. -Zahteve in parametri .[#toc-requirement-and-parameters] +Potrjevanje parametrov .[#toc-validation-of-parameters] ------------------------------------------------------- -Zahteva, ki jo obravnava predstavnik, je objekt [api:Nette\Application\Request] in jo vrne predstavnikova metoda `getRequest()`. Vključuje polje parametrov, vsak od njih pa pripada bodisi kateri od komponent bodisi neposredno predstavniku (ki je pravzaprav tudi komponenta, čeprav posebna). Nette torej parametre prerazporedi in posreduje med posameznimi komponentami (in predstavnikom) tako, da pokliče metodo `loadState(array $params)`. Parametre lahko pridobimo z metodo `getParameters(): array`, posamično pa z metodo `getParameter($name)`. Vrednosti parametrov so nizi ali polja nizov, v bistvu so surovi podatki, pridobljeni neposredno iz naslova URL. +Vrednosti [parametrov zahteve |#request parameters] in [trajnih parametrov |#persistent parameters], prejetih z naslovov URL, se zapišejo v lastnosti z metodo `loadState()`. Preveri tudi, ali se podatkovna vrsta, navedena v lastnosti, ujema, sicer se odzove z napako 404 in stran se ne prikaže. - -Potrjevanje trajnih parametrov .[#toc-validation-of-persistent-parameters] --------------------------------------------------------------------------- - -Vrednosti [trajnih parametrov |#persistent parameters], prejetih z naslovov URL, se zapišejo v lastnosti z metodo `loadState()`. Preveri tudi, ali se podatkovna vrsta, navedena v lastnosti, ujema, sicer se odzove z napako 404 in stran se ne prikaže. - -Nikoli ne zaupajte slepo trajnim parametrom, saj jih lahko uporabnik zlahka prepiše v naslovu URL. Tako na primer preverimo, ali je `$this->lang` med podprtimi jeziki. Dober način za to je, da prekrijete zgoraj omenjeno metodo `loadState()`: +Parametrom nikoli ne zaupajte na slepo, saj jih lahko uporabnik zlahka prepiše v naslovu URL. Tako na primer preverimo, ali je `$this->lang` med podprtimi jeziki. Dober način za to je, da prekrijete zgoraj omenjeno metodo `loadState()`: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,7 +361,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Shranjevanje in obnavljanje zahtevka .[#toc-save-and-restore-the-request] ------------------------------------------------------------------------- -Trenutni zahtevek lahko shranite v sejo ali pa ga obnovite iz seje in omogočite predavatelju, da ga ponovno izvede. To je na primer uporabno, ko uporabnik izpolni obrazec in mu poteče prijava. Da ne bi izgubili podatkov, pred preusmeritvijo na stran za prijavo shranimo trenutno zahtevo v sejo z uporabo `$reqId = $this->storeRequest()`, ki vrne identifikator v obliki kratkega niza in ga kot parameter posreduje predstavniku za prijavo. +Zahteva, ki jo predstavnik obravnava, je objekt [api:Nette\Application\Request] in jo vrne predstavnikova metoda `getRequest()`. + +Trenutni zahtevek lahko shranite v sejo ali pa ga obnovite iz seje in omogočite predstavniku, da ga ponovno izvede. To je na primer uporabno, ko uporabnik izpolni obrazec in mu poteče prijava. Da ne bi izgubili podatkov, pred preusmeritvijo na stran za prijavo shranimo trenutno zahtevo v sejo z uporabo `$reqId = $this->storeRequest()`, ki vrne identifikator v obliki kratkega niza in ga kot parameter posreduje predstavniku za prijavo. Po prijavi pokličemo metodo `$this->restoreRequest($reqId)`, ki prevzame zahtevo iz seje in ji jo posreduje naprej. Metoda preveri, ali je zahtevo ustvaril isti uporabnik, kot je zdaj prijavljeni. Če se prijavi drug uporabnik ali je ključ neveljaven, ne stori ničesar in program se nadaljuje. @@ -362,7 +384,7 @@ Preusmeritev se ne izvede pri zahtevi AJAX ali POST, ker bi povzročila izgubo p Kanonizacijo lahko sprožite tudi ročno z metodo `canonicalize()`, ki tako kot metoda `link()` kot argumente prejme predstavnika, dejanja in parametre. Ustvari povezavo in jo primerja s trenutnim naslovom URL. Če se razlikuje, preusmeri na ustvarjeno povezavo. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // preusmeri, če se $slug razlikuje od $realSlug @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Omejitev dostopa z uporabo `#[Requires]` .[#toc-access-restriction-using-requires]{data-version:3.2.2} +------------------------------------------------------------------------------------------------------ + +. `#[Requires]` atribut zagotavlja napredne možnosti za omejevanje dostopa do predavateljev in njihovih metod. Z njim lahko določite metode HTTP, zahtevate zahteve AJAX, omejite dostop do istega izvora in omejite dostop samo na posredovanje. Atribut je mogoče uporabiti za razrede predstavnikov in posamezne metode, kot so `action()`, `render()`, `handle()`, in `createComponent()`. + +Določite lahko te omejitve: +- za metode HTTP: `#[Requires(methods: ['GET', 'POST'])]` +- ki zahteva zahtevo AJAX: `#[Requires(ajax: true)]` +- dostop samo iz istega izvora: `#[Requires(sameOrigin: true)]` +- dostop samo prek posredovanja: `#[Requires(forward: true)]` +- omejitve za določena dejanja: `#[Requires(actions: 'default')]` + +Za podrobnosti glejte [Kako uporabljati Requires atribut |best-practices:attribute-requires]. + + +Preverjanje metode HTTP .[#toc-http-method-check] +------------------------------------------------- + +V omrežju Nette predstavniki samodejno preverijo metodo HTTP vsake prejete zahteve predvsem iz varnostnih razlogov. Privzeto so dovoljene metode `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. + +Če želite omogočiti dodatne metode, kot je `OPTIONS`, lahko uporabite `#[Requires]` atribut (od različice 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 preveri, ali je metoda, navedena v zahtevi, vključena v polje `$presenter->allowedMethods`. Dodajte metodo, kot je ta: + +```php +class MyPresenter extends Nette\Application\UI\Presenter +{ + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } +} +``` + +Ključnega pomena je poudariti, da če omogočite metodo `OPTIONS`, jo morate ustrezno obdelati tudi v svojem predstavitvenem programu. Ta metoda se pogosto uporablja kot tako imenovana zahteva preflight, ki jo brskalniki samodejno pošljejo pred dejansko zahtevo, ko je treba ugotoviti, ali je zahteva dovoljena z vidika politike CORS (Cross-Origin Resource Sharing). Če to metodo dovolite, vendar ne izvedete ustreznega odziva, lahko pride do nedoslednosti in morebitnih varnostnih težav. + + Nadaljnje branje .[#toc-further-reading] ======================================== diff --git a/application/sl/routing.texy b/application/sl/routing.texy index 0f717178bb..beefb6fba0 100644 --- a/application/sl/routing.texy +++ b/application/sl/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Lahko pa uporabimo tudi to obliko, pri čemer opazimo prepisovanje regularnega izraza za preverjanje: +Za podrobnejšo specifikacijo je mogoče uporabiti še bolj razširjeno obliko, v kateri lahko poleg privzetih vrednosti nastavite tudi druge lastnosti parametrov, na primer regularni izraz za preverjanje (glejte parameter `id` ): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Te bolj zgovorne oblike so uporabne za dodajanje drugih metapodatkov. +Pomembno je opozoriti, da če parametri, opredeljeni v polju, niso vključeni v masko poti, njihovih vrednosti ni mogoče spremeniti, niti z uporabo parametrov poizvedbe, določenih za vprašalnim znakom v naslovu URL. Filtri in prevodi .[#toc-filters-and-translations] @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Moduli .[#toc-modules] ---------------------- -Če imamo več poti, ki pripadajo enemu [modulu |modules], jih lahko z uporabo `withModule()` združimo v skupine: +Če imamo več poti, ki pripadajo enemu [modulu |directory-structure#Presenters and Templates], jih lahko združimo v skupine z uporabo spletne strani `withModule()`: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Integracija .[#toc-integration] =============================== -Da bi naš usmerjevalnik povezali z aplikacijo, moramo o tem obvestiti vsebnik DI. Najlažje je pripraviti tovarno, ki bo zgradila objekt usmerjevalnika, in povedati konfiguraciji vsebnika, naj jo uporabi. Recimo, da v ta namen napišemo metodo `App\Router\RouterFactory::createRouter()`: +Da bi naš usmerjevalnik povezali z aplikacijo, moramo o tem obvestiti vsebnik DI. Najlažje je pripraviti tovarno, ki bo zgradila objekt usmerjevalnika, in povedati konfiguraciji vsebnika, naj jo uporabi. Recimo, da v ta namen napišemo metodo `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ Nato zapišemo v [konfiguracijo |dependency-injection:services]: ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Vse odvisnosti, kot je povezava s podatkovno bazo itd., se metodi tovarne posredujejo kot njeni parametri z uporabo [samodejnega povezovanja |dependency-injection:autowiring]: @@ -663,7 +663,7 @@ Z ločeno uporabo mislimo na uporabo zmožnosti usmerjevalnika v aplikaciji, ki Tako bomo ponovno ustvarili metodo, ki bo zgradila usmerjevalnik, na primer: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Ali pa bomo predmete ustvarili neposredno: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/sl/templates.texy b/application/sl/templates.texy index 66005783a9..7a0cf118e1 100644 --- a/application/sl/templates.texy +++ b/application/sl/templates.texy @@ -34,35 +34,81 @@ To pa je lahko predloga za dejanja: V njej je opredeljen blok `content`, ki se v postavitev vstavi namesto bloka `{include content}`, prav tako pa je na novo opredeljen blok `title`, ki v postavitvi prepiše blok `{block title}`. Poskusite si predstavljati rezultat. -Iskanje predlog .[#toc-search-for-templates] --------------------------------------------- +Iskanje predloge .[#toc-template-lookup] +---------------------------------------- -Pot do predlog se določi po preprosti logiki. Poskusi preveriti, ali obstaja ena od teh datotek s predlogami glede na imenik, v katerem se nahaja razred presenter, kjer `` je ime trenutnega predstavnika in `` je ime trenutnega dejanja: +V predstavitvenih programih vam ni treba določiti, katera predloga naj se prikaže; ogrodje samodejno določi pot, kar vam olajša kodiranje. -- `templates//.latte` -- `templates/..latte` +Če uporabljate imeniško strukturo, v kateri ima vsak predstavnik svoj imenik, preprosto postavite predlogo v ta imenik pod ime dejanja (npr. pogleda). Na primer, za dejanje `default` uporabite predlogo `default.latte`: -Če predloge ne najde, jo poskuša poiskati v imeniku `templates` eno raven višje, tj. na isti ravni kot imenik z razredom predstavnika. +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -Če predloge ne najde niti tam, se kot odgovor prikaže [napaka 404 |presenters#Error 404 etc.]. +Če uporabljate strukturo, v kateri so predstavniki skupaj v enem imeniku, predloge pa v mapi `templates`, jo shranite bodisi v datoteko `..latte` ali . `/.latte`: -Pogled lahko spremenite tudi z uporabo `$this->setView('otherView')`. Lahko pa namesto iskanja neposredno določite ime datoteke s predlogo z uporabo `$this->template->setFile('/path/to/template.latte')`. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- + +Imenik `templates` lahko postavite tudi eno raven višje, na isto raven kot imenik z razredi predavateljev. + +Če predloge ni mogoče najti, se predstavitveni program odzove z [napako 404 - stran ni najdena |presenters#Error 404 etc]. + +Prikaz lahko spremenite z uporabo spletne strani `$this->setView('anotherView')`. Datoteko s predlogo lahko določite tudi neposredno z uporabo `$this->template->setFile('/path/to/template.latte')`. .[note] -Poti, po katerih se iščejo predloge, lahko spremenite tako, da nadgradite metodo [formatTemplateFiles |api:Nette\Application\UI\Presenter::formatTemplateFiles()], ki vrne polje možnih poti do datotek. +Datoteke, v katerih se iščejo predloge, lahko spremenite tako, da nadgradite metodo [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], ki vrne polje možnih imen datotek. + + +Iskanje predlog za postavitev .[#toc-layout-template-lookup] +------------------------------------------------------------ + +Nette samodejno poišče tudi datoteko z maketo. + +Če uporabljate imeniško strukturo, v kateri ima vsak predavatelj svoj imenik, postavite postavitev bodisi v mapo s predavateljem, če je namenjena samo njemu, bodisi za stopnjo višje, če je skupna več predavateljem: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +Če uporabljate strukturo, v kateri so predstavniki združeni v enem imeniku, predloge pa so v mapi `templates`, bo postavitev pričakovana na naslednjih mestih: -Postavitev se pričakuje v naslednjih datotekah: +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` postavitev, ki je skupna več predstavnikom +Če je predvajalnik v modulu, bo poiskal tudi naprej po drevesu imenikov v skladu z vgnezditvijo modula. -`` je ime trenutnega predavatelja in `` je ime postavitve, ki je privzeto `'layout'`. Ime lahko spremenite s `$this->setLayout('otherLayout')`, tako da se bodo poskušale uporabiti datoteke `@otherLayout.latte`. +Ime postavitve lahko spremenite z uporabo spletne strani `$this->setLayout('layoutAdmin')`, nato pa jo boste pričakali v datoteki `@layoutAdmin.latte`. Datoteko s predlogo postavitve lahko določite tudi neposredno z uporabo `$this->setLayout('/path/to/template.latte')`. -Ime datoteke za predlogo postavitve lahko določite tudi neposredno z uporabo `$this->setLayout('/path/to/template.latte')`. Z uporabo spletne strani `$this->setLayout(false)` bo iskanje postavitve onemogočeno. +Uporaba `$this->setLayout(false)` ali oznake `{layout none}` znotraj predloge onemogoči iskanje postavitve. .[note] -Poti, po katerih se iščejo predloge, lahko spremenite tako, da nadgradite metodo [formatLayoutTemplateFiles |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], ki vrne polje možnih poti do datotek. +Datoteke, v katerih se iščejo predloge postavitve, lahko spremenite tako, da nadgradite metodo [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], ki vrne polje možnih imen datotek. Spremenljivke v predlogi .[#toc-variables-in-the-template] @@ -104,7 +150,7 @@ Opomba `@property-read` je namenjena IDE in statični analizi, zaradi nje bo del Privoščite si lahko tudi razkošje šepetanja v predlogah, samo namestite vtičnik Latte v PhpStorm in na začetku predloge navedite ime razreda, glejte članek "Latte: kako vtipkati sistem":https://blog.nette.org/sl/latte-kako-uporabljati-sistem-tipov: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void Različica Latte 3 ponuja naprednejši način z ustvarjanjem [razširitve |latte:creating-extension] za vsak spletni projekt. Tukaj je približni primer takega razreda: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ Registriramo ga z uporabo [konfiguracije|configuration#Latte]: ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ Prevajalnik lahko nastavimo tudi s [konfiguracijo |configuration#Latte]: ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` Prevajalnik lahko nato uporabimo na primer kot filter `|translate`, pri čemer metodi `translate()` posredujemo dodatne parametre (glej `foo, bar`): diff --git a/application/tr/@home.texy b/application/tr/@home.texy index bb31500e7c..e7b82598fe 100644 --- a/application/tr/@home.texy +++ b/application/tr/@home.texy @@ -1,36 +1,85 @@ -Nette Application -***************** +Nette Uygulama +************** .[perex] -`nette/application` paketi, etkileşimli web uygulamaları oluşturmak için temel oluşturur. +Nette Application, modern web uygulamaları oluşturmak için güçlü araçlar getiren Nette çerçevesinin çekirdeğidir. Geliştirmeyi önemli ölçüde basitleştiren ve kod güvenliğini ve sürdürülebilirliğini artıran çok sayıda olağanüstü özellik sunar. -- [Uygulamalar nasıl çalışır? |how-it-works] -- [Bootstrap |Bootstrap] -- [Sunum Yapanlar |Presenters] -- [Şablonlar |Templates] -- [Modüller |Modules] -- [Yönlendirme |Routing] -- [URL Bağlantıları Oluşturma |creating-links] -- [İnteraktif Bileşenler |components] -- [AJAX ve Snippet'ler |ajax] -- [Çarpan |multiplier] -- [Konfigürasyon |Configuration] +Kurulum .[#toc-installation] +---------------------------- -Kurulum -------- - -[Composer'ı |best-practices:composer] kullanarak paketi indirin ve yükleyin: + [Composer |best-practices:composer]'ı kullanarak kütüphaneyi indirin ve kurun: ```shell composer require nette/application ``` + +Neden Nette Uygulamasını Seçmelisiniz? .[#toc-why-choose-nette-application] +--------------------------------------------------------------------------- + +Nette, web teknolojilerinde her zaman öncü olmuştur. + +**Çift Yönlü Yönlendirici:** Nette, çift yönlülüğü açısından benzersiz olan gelişmiş bir yönlendirme sistemine sahiptir - yalnızca URL'leri uygulama eylemlerine çevirmekle kalmaz, aynı zamanda tersine URL'ler de oluşturabilir. Bu şu anlama gelir: +- Şablon dosyalarını değiştirmeden tüm uygulamanın URL yapısını istediğiniz zaman değiştirebilirsiniz +- URL'ler otomatik olarak kanonikleştirilerek SEO iyileştirilir +- Yönlendirme tek bir yerde tanımlanır, ek açıklamalar içinde dağınık değildir + +**Bileşenler ve Sinyaller:** Delphi ve React.js'den ilham alan yerleşik bileşen sistemi, PHP çerçeveleri arasında benzersizdir: +- Yeniden kullanılabilir UI öğeleri oluşturmayı sağlar +- Hiyerarşik bileşen kompozisyonunu destekler +- Sinyalleri kullanarak zarif AJAX istek işleme sunar +- Componette](https://componette.org)üzerinde hazır bileşenlerden oluşan zengin kütüphane + +**AJAX ve Snippets:** Nette, 2009 yılında, Ruby on Rails için Hotwire veya Symfony UX Turbo gibi çözümlerden önce AJAX ile çalışmanın devrim niteliğinde bir yolunu tanıttı: +- Snippet'ler, JavaScript yazmadan sayfanın yalnızca bazı bölümlerinin güncellenmesini sağlar +- Bileşen sistemi ile otomatik entegrasyon +- Sayfa bölümlerinin akıllı geçersiz kılınması +- Minimum veri aktarımı + +**Sezgisel [Latte |latte:] Şablonları:** PHP için gelişmiş özelliklere sahip en güvenli şablonlama sistemi: +- İçeriğe duyarlı kaçış ile otomatik XSS koruması +- Özel filtreler, fonksiyonlar ve etiketlerle genişletilebilir +- AJAX için şablon kalıtımı ve parçacıkları +- Tip sistemi ile mükemmel PHP 8.x desteği + +**Bağımlılık Enjeksiyonu:** Nette, Bağımlılık Enjeksiyonunu tamamen kullanır: +- Otomatik bağımlılık geçişi (autowiring) +- Net NEON formatı kullanılarak yapılandırma +- Bileşen fabrikaları için destek + + +Temel Faydalar .[#toc-main-benefits] +------------------------------------ + +- Güvenlik**: XSS, CSRF gibi [güvenlik açıklarına |nette:vulnerability-protection] karşı otomatik koruma. +- Verimlilik**: Akıllı tasarım sayesinde daha az yazı, daha fazla özellik +- Hata ayıklama**: Yönlendirme panelli [Tracy hata ayıklayıcı |tracy:] +- **Performans**: Akıllı önbellekleme sistemi, bileşenlerin tembel yüklenmesi +- Esneklik**: Uygulama tamamlandıktan sonra bile kolay URL değişikliği +- Bileşenler**: Yeniden kullanılabilir kullanıcı arayüzü öğelerinden oluşan benzersiz sistem +- Modern**: PHP 8.4+ ve tip sistemi için tam destek + + +Başlarken .[#toc-getting-started] +--------------------------------- + +1. [Uygulamaları Anlamak |how-it-works] - Temel mimariyi anlamak +2. [Sunum |presenters] yapanlar - Sunum yapanlar ve eylemlerle çalışma +3. [Şablonlar |templates] - Latte'de şablon oluşturma +4. [Yönlendirme |routing] - URL yapılandırması +5. [İnteraktif Bileşenler |components] - Bileşen sistemini kullanma + + +PHP Uyumluluğu .[#toc-php-compatibility] +---------------------------------------- + | sürüm | PHP ile uyumlu |-----------|------------------- -| Nette Uygulama 4.0 | PHP 8.0 - 8.2 -| Nette Uygulama 3.1 | PHP 7.2 - 8.2 +| Nette Uygulama 4.0 | PHP 8.1 - 8.4 +| Nette Uygulama 3.2 | PHP 8.1 - 8.4 +| Nette Uygulama 3.1 | PHP 7.2 - 8.3 | Nette Uygulama 3.0 | PHP 7.1 - 8.0 -| Nette Uygulama 2.4 | PHP 5.6 - 8.0 +| Nette Uygulaması 2.4 | PHP 5.6 - 8.0 -En son yama sürümleri için geçerlidir. +En son yama sürümleri için geçerlidir. \ No newline at end of file diff --git a/application/tr/@left-menu.texy b/application/tr/@left-menu.texy index b64011d843..c81d8eccda 100644 --- a/application/tr/@left-menu.texy +++ b/application/tr/@left-menu.texy @@ -4,7 +4,7 @@ Nette Uygulama - [Bootstrap |Bootstrap] - [Sunum Yapanlar |Presenters] - [Şablonlar |Templates] -- [Modüller |Modules] +- [Dizin yapısı |directory-structure] - [Yönlendirme |Routing] - [URL Bağlantıları Oluşturma |creating-links] - [İnteraktif Bileşenler |components] diff --git a/application/tr/ajax.texy b/application/tr/ajax.texy index 2502de3b69..007283e44d 100644 --- a/application/tr/ajax.texy +++ b/application/tr/ajax.texy @@ -3,10 +3,10 @@ AJAX ve Snippet'ler
    -Günümüzde modern web uygulamalarının yarısı sunucuda, yarısı da tarayıcıda çalışmaktadır. AJAX hayati bir birleştirici faktördür. Nette Framework ne gibi destekler sunuyor? -- Şablon parçaları gönderme (*snippet* olarak adlandırılır) +İşlevselliğin genellikle sunucu ve tarayıcı arasında yayıldığı modern web uygulamaları çağında, AJAX önemli bir bağlantı unsurudur. Nette Framework bu alanda hangi seçenekleri sunuyor? +- Şablonun snippet olarak adlandırılan parçalarını gönderme - PHP ve JavaScript arasında değişken aktarımı -- AJAX uygulamaları hata ayıklama +- AJAX isteklerinde hata ayıklama araçları
    @@ -14,29 +14,32 @@ Günümüzde modern web uygulamalarının yarısı sunucuda, yarısı da tarayı AJAX İsteği .[#toc-ajax-request] ================================ -Bir AJAX isteği klasik bir istekten farklı değildir - sunum yapan kişi belirli bir görünüm ve parametrelerle çağrılır. Buna nasıl yanıt verileceği de sunucuya bağlıdır: bir HTML kod parçası (HTML snippet), bir XML belgesi, bir JSON nesnesi veya JavaScript kodu döndüren kendi rutinini kullanabilir. +Bir AJAX isteği temelde klasik bir HTTP isteğinden farklı değildir. Bir sunucu belirli parametrelerle çağrılır. İsteğe nasıl yanıt verileceği sunucuya bağlıdır - JSON biçiminde veri döndürebilir, HTML kodunun bir bölümünü, bir XML belgesini vb. gönderebilir. -Sunucu tarafında, bir AJAX isteği `$httpRequest->isAjax()` [HTTP isteğini kapsülleyen |http:request] hizmet yöntemi kullanılarak algılanabilir ( `X-Requested-With` HTTP başlığına dayalı olarak algılar). Sunucunun içinde, `$this->isAjax()` yöntemi şeklinde bir kısayol mevcuttur. +Tarayıcı tarafında, `fetch()` işlevini kullanarak bir AJAX isteği başlatıyoruz: -JSON'da tarayıcıya veri göndermeye adanmış `payload` adında önceden işlenmiş bir nesne vardır. - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // yanıtın işlenmesi +}); ``` -JSON çıktınız üzerinde tam kontrol sahibi olmak için sunumunuzda `sendJson` yöntemini kullanın. Sunucuyu hemen sonlandırır ve şablon olmadan yaparsınız: +Sunucu tarafında, bir AJAX isteği, [HTTP isteğini kapsülleyen |http:request] hizmetin `$httpRequest->isAjax()` yöntemi tarafından tanınır. Bu yöntem `X-Requested-With` HTTP başlığını kullanır, bu yüzden bunu göndermek çok önemlidir. Sunucu içinde `$this->isAjax()` yöntemini kullanabilirsiniz. + +Verileri JSON biçiminde göndermek istiyorsanız [`sendJson()` |presenters#Sending a response] yöntemi. Yöntem ayrıca sunum yapan kişinin etkinliğini de sonlandırır. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -HTML göndermek istiyorsak, AJAX istekleri için özel bir şablon ayarlayabiliriz: +AJAX için tasarlanmış özel bir şablonla yanıt vermeyi planlıyorsanız, bunu aşağıdaki şekilde yapabilirsiniz: ```php public function handleClick($param): void @@ -44,27 +47,43 @@ public function handleClick($param): void if ($this->isAjax()) { $this->template->setFile('path/to/ajax.latte'); } - // ... + //... } ``` +Parçacıklar .[#toc-snippets] +============================ + +Sunucuyu istemciye bağlamak için Nette tarafından sunulan en güçlü araç snippet'lerdir. Bunlarla, sıradan bir uygulamayı minimum çaba ve birkaç satır kodla bir AJAX uygulamasına dönüştürebilirsiniz. Fifteen örneği tüm bunların nasıl çalıştığını göstermektedir ve kodu [GitHub |https://github.com/nette-examples/fifteen]'da bulunabilir. + +Parçacıklar veya kırpmalar, tüm sayfayı yeniden yüklemek yerine sayfanın yalnızca bazı bölümlerini güncellemenize olanak tanır. Bu daha hızlı ve daha verimlidir ve ayrıca daha rahat bir kullanıcı deneyimi sağlar. Snippet'ler size Ruby on Rails için Hotwire veya Symfony UX Turbo'yu hatırlatabilir. İlginç bir şekilde, Nette snippet'leri 14 yıl önce tanıtmıştı. + +Parçacıklar nasıl çalışır? Sayfa ilk yüklendiğinde (AJAX olmayan bir istek), tüm snippet'ler dahil olmak üzere sayfanın tamamı yüklenir. Kullanıcı sayfayla etkileşime girdiğinde (örneğin, bir düğmeye tıkladığında, bir form gönderdiğinde vb.), sayfanın tamamını yüklemek yerine bir AJAX isteği yapılır. Sunucudaki kod eylemi gerçekleştirir ve hangi parçacıkların güncellenmesi gerektiğine karar verir. Nette bu parçacıkları işler ve bir JSON dizisi şeklinde gönderir. Tarayıcıdaki işleme kodu daha sonra alınan parçacıkları sayfaya geri ekler. Bu nedenle, yalnızca değiştirilen parçacıkların kodu aktarılır, bant genişliğinden tasarruf edilir ve tüm sayfa içeriğinin aktarılmasına kıyasla yükleme hızlandırılır. + + Naja .[#toc-naja] -================= +----------------- -[Naja kütüphanesi |https://naja.js.org], tarayıcı tarafında AJAX isteklerini işlemek için kullanılır. Bir node.js paketi olarak [yükleyin |https://naja.js.org/#/guide/01-install-setup-naja] (Webpack, Rollup, Vite, Parcel ve daha fazlası ile kullanmak için): +Tarayıcı tarafında snippet'leri işlemek için [Naja kütüphanesi |https://naja.js.org] kullanılır. [Bunu |https://naja.js.org/#/guide/01-install-setup-naja] bir node.js paketi olarak [yükleyin |https://naja.js.org/#/guide/01-install-setup-naja] (Webpack, Rollup, Vite, Parcel ve diğerleri gibi uygulamalarla kullanmak için): ```shell npm install naja ``` -...veya doğrudan sayfa şablonuna ekleyin: +... veya doğrudan sayfa şablonuna ekleyin: ```html ``` -Normal bir bağlantıdan (sinyal) veya form gönderiminden bir AJAX isteği oluşturmak için ilgili bağlantıyı, formu veya düğmeyi `ajax` sınıfıyla işaretlemeniz yeterlidir: +Öncelikle kütüphaneyi [başlatmanız |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] gerekir: + +```js +naja.initialize(); +``` + +Sıradan bir bağlantıyı (sinyal) veya form gönderimini AJAX isteği haline getirmek için ilgili bağlantıyı, formu veya düğmeyi `ajax` sınıfıyla işaretlemeniz yeterlidir: ```html Go @@ -74,64 +93,39 @@ Normal bir bağlantıdan (sinyal) veya form gönderiminden bir AJAX isteği olu or +
    ``` -Parçacıklar .[#toc-snippets] -============================ - -Yerleşik AJAX desteğinin çok daha güçlü bir aracı vardır - snippet'ler. Bunları kullanmak, normal bir uygulamayı yalnızca birkaç satır kod kullanarak bir AJAX uygulamasına dönüştürmeyi mümkün kılar. Tüm bunların nasıl çalıştığı, koduna derlemede veya [GitHub'da |https://github.com/nette-examples/fifteen] da erişilebilen Fifteen örneğinde gösterilmiştir. - -Snippet'lerin çalışma şekli, ilk (yani AJAX olmayan) istek sırasında tüm sayfanın aktarılması ve ardından her AJAX [alt |components#signal] isteğinde (aynı sunucunun aynı görünümünün isteği) yalnızca değiştirilen parçaların kodunun daha önce bahsedilen `payload` deposuna aktarılmasıdır. +Parçacıkları Yeniden Çizme .[#toc-redrawing-snippets] +----------------------------------------------------- -Snippet'ler size Ruby on Rails için Hotwire veya Symfony UX Turbo'yu hatırlatabilir, ancak Nette bunları on dört yıl önce buldu. - - -Snippet'lerin Geçersiz Kılınması .[#toc-invalidation-of-snippets] -================================================================= - -[Control |components] sınıfının her bir torunu (ki bir Presenter da öyledir), bir istek sırasında yeniden oluşturmasını gerektiren herhangi bir değişiklik olup olmadığını hatırlayabilir. Bunu işlemek için bir çift yöntem vardır: `redrawControl()` ve `isControlInvalid()`. Bir örnek: + [Control |components] sınıfının her nesnesi (Presenter'ın kendisi de dahil olmak üzere) yeniden çizilmesini gerektirecek değişikliklerin olup olmadığının kaydını tutar. Bu amaç için `redrawControl()` yöntemi kullanılır. ```php public function handleLogin(string $user): void { - // Nesne, kullanıcı giriş yaptıktan sonra yeniden oluşturulmalıdır + // giriş yaptıktan sonra ilgili kısmı yeniden çizmek gerekir $this->redrawControl(); - // ... + //... } ``` -Ancak Nette, tüm bileşenlerden daha da ince bir çözünürlük sunar. Listelenen yöntemler, isteğe bağlı bir parametre olarak "snippet" olarak adlandırılan bir ismi kabul eder. Bir "snippet" temel olarak şablonunuzdaki bir Latte makrosu tarafından bu amaç için işaretlenmiş bir öğedir, daha sonra bu konuda daha fazla bilgi verilecektir. Böylece bir bileşenden şablonunun sadece *parçalarını* yeniden çizmesini istemek mümkündür. Bileşenin tamamı geçersiz kılınırsa, tüm parçacıkları yeniden oluşturulur. Bir bileşen, alt bileşenlerinden herhangi birinin geçersiz olması durumunda da "geçersiz" olur. - -```php -$this->isControlInvalid(); // -> false -$this->redrawControl('header'); // 'header' adlı parçacığı geçersiz kılar -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, en az bir snippet geçersiz +Nette ayrıca nelerin yeniden çizilmesi gerektiğinin daha iyi kontrol edilmesini sağlar. Yukarıda bahsedilen yöntem, snippet adını bir argüman olarak alabilir. Böylece, şablon parçası düzeyinde geçersiz kılmak (yani yeniden çizmeye zorlamak) mümkündür. Bileşenin tamamı geçersiz kılınırsa, her parçacığı da yeniden çizilir: -$this->redrawControl(); // tüm bileşeni, her parçacığı geçersiz kılar -$this->isControlInvalid('footer'); // -> true +```php +// 'header' parçacığını geçersiz kılar +$this->redrawControl('header'); ``` -Sinyal alan bir bileşen otomatik olarak yeniden çizilmek üzere işaretlenir. - -Snippet yeniden çizimi sayesinde hangi öğelerin hangi kısımlarının yeniden işlenmesi gerektiğini tam olarak biliyoruz. - - -Etiket `{snippet} … {/snippet}` .{toc: Tag snippet} -=================================================== -Sayfanın oluşturulması normal bir istekle çok benzer şekilde ilerler: aynı şablonlar yüklenir, vb. Ancak önemli olan, çıktıya ulaşmaması gereken kısımların dışarıda bırakılmasıdır; diğer kısımlar bir tanımlayıcı ile ilişkilendirilmeli ve bir JavaScript işleyicisi için anlaşılabilir bir biçimde kullanıcıya gönderilmelidir. +Latte'de Parçacıklar .[#toc-snippets-in-latte] +---------------------------------------------- - -Sözdizimi .[#toc-syntax] ------------------------- - -Şablonda bir kontrol veya snippet varsa, bunu `{snippet} ... {/snippet}` pair etiketini kullanarak sarmalıyız - bu, işlenen snippet'in "kesilip çıkarılmasını" ve tarayıcıya gönderilmesini sağlayacaktır. Ayrıca onu bir yardımcı içine alacaktır `
    ` etiketi (farklı bir etiket kullanmak mümkündür). Aşağıdaki örnekte `header` adında bir snippet tanımlanmıştır. Bir bileşenin şablonunu da temsil edebilir: +Latte'de snippet'leri kullanmak son derece kolaydır. Şablonun bir bölümünü snippet olarak tanımlamak için, onu `{snippet}` ve `{/snippet}` etiketlerine sarmanız yeterlidir: ```latte {snippet header} @@ -139,7 +133,9 @@ Sözdizimi .[#toc-syntax] {/snippet} ``` -dışındaki bir türden bir snippet `
    ` veya ek HTML öznitelikleri içeren bir snippet, öznitelik varyantı kullanılarak elde edilir: +Kod parçacığı bir öğe oluşturur `
    ` HTML sayfasında özel olarak oluşturulmuş bir `id`. Bir snippet yeniden çizilirken, bu öğenin içeriği güncellenir. Bu nedenle, sayfa ilk kez render edildiğinde, başlangıçta boş olsalar bile tüm snippet'ler de render edilmelidir. + +dışında bir öğe ile de bir snippet oluşturabilirsiniz `
    ` bir n:özniteliği kullanarak: ```latte
    @@ -148,138 +144,106 @@ dışındaki bir türden bir snippet `
    ` veya ek HTML öznitelikleri içeren ``` -Dinamik Parçacıklar .[#toc-dynamic-snippets] -============================================ +Snippet Alanları .[#toc-snippet-areas] +-------------------------------------- -Nette'de ayrıca bir çalışma zamanı parametresine dayalı olarak dinamik bir adla snippet'ler tanımlayabilirsiniz. Bu, sadece bir satırı değiştirmemiz gereken ancak tüm listeyi onunla birlikte aktarmak istemediğimiz çeşitli listeler için en uygun olanıdır. Bunun bir örneği şöyle olabilir: +Snippet adları ifadeler de olabilir: ```latte -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Birkaç dinamik snippet içeren `itemsContainer` adında bir statik snippet vardır: `item-0`, `item-1` ve benzeri. +Bu şekilde, `item-0`, `item-1`, vb. gibi birkaç snippet elde edeceğiz. Dinamik bir snippet'i (örneğin, `item-1`) doğrudan geçersiz kılacak olursak, hiçbir şey yeniden çizilmeyecektir. Bunun nedeni, snippet'lerin gerçek alıntılar olarak işlev görmesi ve yalnızca kendilerinin doğrudan render edilmesidir. Ancak, şablonda teknik olarak `item-1` adında bir snippet yoktur. Yalnızca snippet'in çevresindeki kod, bu durumda foreach döngüsü çalıştırıldığında ortaya çıkar. Bu nedenle, şablonun çalıştırılması gereken kısmını `{snippetArea}` etiketiyle işaretleyeceğiz: -Dinamik bir snippet'i doğrudan yeniden çizemezsiniz ( `item-1` adresinin yeniden çizilmesinin hiçbir etkisi yoktur), üst snippet'ini yeniden çizmeniz gerekir (bu örnekte `itemsContainer`). Bu, üst snippet'in kodunun yürütülmesine neden olur, ancak daha sonra tarayıcıya yalnızca alt snippet'leri gönderilir. Alt snippet'lerden yalnızca birini göndermek istiyorsanız, diğer alt snippet'leri oluşturmamak için ana snippet'in girdisini değiştirmeniz gerekir. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -Yukarıdaki örnekte, bir AJAX isteği için `$list` dizisine yalnızca bir öğe ekleneceğinden emin olmanız gerekir, bu nedenle `foreach` döngüsü yalnızca bir dinamik parçacık yazdıracaktır. +Ve hem bireysel parçacığı hem de tüm alanı yeniden çizeceğiz: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * This method returns data for the list. - * Usually this would just request the data from a model. - * For the purpose of this example, the data is hard-coded. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +Ayrıca `$items` dizisinin yalnızca yeniden çizilmesi gereken öğeleri içerdiğinden emin olmak da önemlidir. -Dahil Edilen Bir Şablondaki Parçacıklar .[#toc-snippets-in-an-included-template] -================================================================================ - -Snippet, farklı bir şablondan dahil edilen bir şablonda olabilir. Bu durumda, ikinci şablondaki dahil etme kodunu `snippetArea` makrosuyla sarmamız gerekir, ardından hem snippetArea'yı hem de gerçek snippet'i yeniden çizeriz. - -Makro `snippetArea`, içindeki kodun yürütülmesini sağlar, ancak tarayıcıya yalnızca dahil edilen şablondaki gerçek parçacık gönderilir. +Parçacıkları olan `{include}` etiketini kullanarak ana şablona başka bir şablon eklerken, dahil edilen şablonu tekrar bir `snippetArea` içine sarmak ve hem parçacığı hem de alanı birlikte geçersiz kılmak gerekir: ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* dahil.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -Ayrıca dinamik snippet'lerle de birleştirebilirsiniz. - - -Ekleme ve Silme .[#toc-adding-and-deleting] -=========================================== -Listeye yeni bir öğe ekler ve `itemsContainer` adresini geçersiz kılarsanız, AJAX isteği yenisini de içeren parçacıkları döndürür, ancak javascript işleyicisi bunu işleyemez. Bunun nedeni, yeni oluşturulan ID'ye sahip bir HTML öğesi olmamasıdır. +Bileşenlerdeki Snippet'ler .[#toc-snippets-in-components] +--------------------------------------------------------- -Bu durumda, en basit yol tüm listeyi bir parçacığa daha sarmak ve hepsini geçersiz kılmaktır: + [Bileşenler |components] içinde parçacıklar oluşturabilirsiniz ve Nette bunları otomatik olarak yeniden çizer. Bununla birlikte, belirli bir sınırlama vardır: parçacıkları yeniden çizmek için, herhangi bir parametre olmadan `render()` yöntemini çağırır. Bu nedenle, şablonda parametre geçmek işe yaramayacaktır: ```latte -{snippet wholeList} -
      - {foreach $list as $id => $item} -
    • {$item} update
    • - {/foreach} -
    -{/snippet} -Add +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Kullanıcı Verilerinin Gönderilmesi .[#toc-sending-user-data] +------------------------------------------------------------ + +Parçacıkların yanı sıra, istemciye herhangi bir ek veri gönderebilirsiniz. Bunları `payload` nesnesine yazmanız yeterlidir: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + //... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -Aynı şey bir öğeyi silmek için de geçerlidir. Boş snippet göndermek mümkün olabilir, ancak genellikle listeler sayfalandırılabilir ve bir öğeyi silip diğerini (sayfalandırılmış listenin farklı bir sayfasında bulunan) yüklemek karmaşık olacaktır. - -Bileşene Parametre Gönderme .[#toc-sending-parameters-to-component] -=================================================================== +Parametreleri Gönderme .[#toc-sending-parameters] +================================================= AJAX isteği aracılığıyla bileşene parametreler gönderdiğimizde, ister sinyal parametreleri ister kalıcı parametreler olsun, bileşenin adını da içeren global adlarını sağlamalıyız. Parametrenin tam adı `getParameterId()` yöntemini döndürür. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -Ve bileşende karşılık gelen parametrelerle yöntemi işleyin. +Bileşende ilgili parametreleri içeren bir tutamaç yöntemi: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/tr/bootstrap.texy b/application/tr/bootstrap.texy index 403f4ee3f7..73fe1a09fe 100644 --- a/application/tr/bootstrap.texy +++ b/application/tr/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + private Configurator $configurator; + private string $rootDir; + + public function __construct() + { + $this->rootDir = dirname(__DIR__); + // Yapılandırıcı, uygulama ortamını ve hizmetlerini ayarlamaktan sorumludur. + $this->configurator = new Configurator; + // Nette tarafından oluşturulan geçici dosyalar için dizini ayarlayın (örn. derlenmiş şablonlar) + $this->configurator->setTempDirectory($this->rootDir . '/temp'); + } + + public function bootWebApplication(): Nette\DI\Container + { + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); + } + + private function initializeEnvironment(): void { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // Nette akıllıdır ve geliştirme modu otomatik olarak açılır, + // ya da aşağıdaki satırın yorumunu kaldırarak belirli bir IP adresi için etkinleştirebilirsiniz: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Tracy'yi etkinleştirir: nihai "İsviçre çakısı" hata ayıklama aracı. + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: verilen dizindeki tüm sınıfları otomatik yükler + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Yapılandırma dosyalarını yükle + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php .[#toc-index-php] =========================== -Web uygulamaları söz konusu olduğunda, başlangıç dosyası `index.php` olup `www/` genel dizininde bulunur. Ortamı başlatmak için `Bootstrap` sınıfına izin verir ve DI konteynerini oluşturan `$configurator` sınıfını döndürür. Daha sonra web uygulamasını çalıştıran `Application` hizmetini elde eder: +Web uygulamaları söz konusu olduğunda, birincil dosya `index.php` olup `www/`[genel dizininde |directory-structure#public-directory-www] bulunur. Bu, Bootstrap sınıfının ortamı başlatmasını ve bir DI konteyneri üretmesini sağlayacaktır. Daha sonra web uygulamasını başlatan `Application` hizmetini alır: ```php -// ortamı başlat + Configurator nesnesini al -$configurator = App\Bootstrap::boot(); -// bir DI konteyneri oluşturun -$container = $configurator->createContainer(); -// DI container bir Nette\Application\Application nesnesi oluşturur +$bootstrap = new App\Bootstrap; +// Ortamı başlatma + bir DI konteyneri oluşturma +$container = $bootstrap->bootWebApplication(); +// DI konteyneri bir Nette\Application\Application nesnesi oluşturur $application = $container->getByType(Nette\Application\Application::class); -// Nette uygulamasını başlat +// Nette uygulamasını başlatın ve gelen isteği işleyin $application->run(); ``` @@ -59,26 +84,42 @@ Gördüğünüz gibi, şimdi daha ayrıntılı olarak tanıtacağımız [api:Net Geliştirme ve Üretim Modu .[#toc-development-vs-production-mode] ================================================================ -Nette, bir talebin yürütüldüğü iki temel mod arasında ayrım yapar: geliştirme ve üretim. Geliştirme modu programcının maksimum konforuna odaklanır, Tracy görüntülenir, şablonlar veya DI konteyner yapılandırması değiştirilirken önbellek otomatik olarak güncellenir, vb. Üretim modu performansa odaklanır, Tracy yalnızca hataları günlüğe kaydeder ve şablonların ve diğer dosyaların değişiklikleri kontrol edilmez. +Nette, bir geliştirme veya üretim sunucusunda çalışmasına bağlı olarak farklı davranır: + +🛠️ Geliştirme Modu: + - Tracy hata ayıklama çubuğunu yararlı bilgilerle birlikte görüntüler (örn. SQL sorguları, yürütme süresi, bellek kullanımı). + - Bir hata oluştuğunda işlev çağrısı izlerini ve değişken içeriklerini içeren ayrıntılı bir hata sayfası gösterir. + - Latte şablonları, yapılandırma dosyaları vb. değiştirildiğinde önbelleği otomatik olarak yeniler. + + +🚀 Üretim Modu: + - Herhangi bir hata ayıklama bilgisi göstermez; tüm hatalar günlüğe kaydedilir. + - Bir hata oluştuğunda `ErrorPresenter` veya genel bir "Sunucu Hatası" sayfası gösterir. + - Önbellek asla otomatik olarak yenilenmez! + - Hız ve güvenlik için optimize edilmiştir. -Mod seçimi otomatik algılama ile yapılır, bu nedenle genellikle herhangi bir şeyi manuel olarak yapılandırmaya veya değiştirmeye gerek yoktur. Uygulama localhost üzerinde çalışıyorsa (yani IP adresi `127.0.0.1` veya `::1`) ve proxy yoksa (yani HTTP başlığı) mod geliştirmedir. Aksi takdirde, üretim modunda çalışır. + +Mod otomatik olarak belirlenir, bu nedenle çoğu durumda manuel olarak yapılandırmaya veya değiştirmeye gerek yoktur: + +- Geliştirme modu: Bir proxy kullanılmadığı sürece (yani HTTP üstbilgilerine göre) localhost'ta (IP adresi `127.0.0.1` veya `::1`) etkin. +- Üretim modu: Diğer her yerde aktif. Geliştirme modunu diğer durumlarda, örneğin belirli bir IP adresinden erişen programcılar için etkinleştirmek istiyorsanız, `setDebugMode()` adresini kullanabilirsiniz: ```php -$configurator->setDebugMode('23.75.345.200'); // bir veya daha fazla IP adresi +$this->configurator->setDebugMode('23.75.345.200'); // bir veya daha fazla IP adresi ``` Bir IP adresini bir çerezle birleştirmenizi kesinlikle öneririz. `nette-debug` çerezine gizli bir belirteç depolayacağız, örneğin `secret1234` ve geliştirme modu, bu IP ve çerez kombinasyonuna sahip programcılar için etkinleştirilecektir. ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` Ayrıca localhost için bile geliştirici modunu tamamen kapatabiliriz: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` `true` değerinin, bir üretim sunucusunda asla gerçekleşmemesi gereken geliştirici modunu zorlayarak açtığını unutmayın. @@ -90,7 +131,7 @@ Hata Ayıklama Aracı Tracy .[#toc-debugging-tool-tracy] Kolay hata ayıklama için harika araç [Tracy'yi |tracy:] açacağız. Geliştirici modunda hataları görselleştirir ve üretim modunda hataları belirtilen dizine kaydeder: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ Geçici Dosyalar .[#toc-temporary-files] Nette, DI konteyneri, RobotLoader, şablonlar vb. için önbelleği kullanır. Bu nedenle, önbelleğin depolanacağı dizinin yolunu ayarlamak gerekir: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` Linux veya macOS üzerinde, `log/` ve `temp/` dizinleri için [yazma izinlerini |nette:troubleshooting#Setting directory permissions] ayarlayın. @@ -112,7 +153,7 @@ RobotLoader .[#toc-robotloader] Genellikle, [RobotLoader'ı |robot-loader:] kullanarak sınıfları otomatik olarak yüklemek isteyeceğiz, bu yüzden onu başlatmalı ve `Bootstrap.php` 'un bulunduğu dizinden (yani `__DIR__`) ve tüm alt dizinlerinden sınıfları yüklemesine izin vermeliyiz: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ Zaman Dilimi .[#toc-timezone] Yapılandırıcı, uygulamanız için bir saat dilimi belirlemenize olanak tanır. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ Geliştirme modunda, kodu veya yapılandırma dosyalarını her değiştirdiğin Yapılandırma dosyaları `addConfig()` kullanılarak yüklenir: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` Birden fazla dosya eklemek için `addConfig()` yöntemi birden fazla kez çağrılabilir. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ Statik Parametreler .[#toc-static-parameters] Yapılandırma dosyalarında kullanılan parametreler [`parameters` bölümünde |dependency-injection:configuration#parameters] tanımlanabilir ve ayrıca `addStaticParameters()` yöntemi ( `addParameters()` takma adı vardır) tarafından geçirilebilir (veya üzerine yazılabilir). Farklı parametre değerlerinin ek DI konteynerlerinin, yani ek sınıfların oluşturulmasına neden olması önemlidir. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ Dinamik Parametreler .[#toc-dynamic-parameters] Konteynere dinamik parametreler de ekleyebiliriz, statik parametrelerin aksine farklı değerleri yeni DI konteynerlerinin oluşturulmasına neden olmaz. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ Ortam değişkenleri dinamik parametreler kullanılarak kolayca kullanılabilir hale getirilebilir. Bunlara yapılandırma dosyalarındaki `%env.variable%` adresinden erişebiliriz. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ Yapılandırma dosyalarında aşağıdaki statik parametreleri kullanabilirsiniz - `%wwwDir%`, `index.php` giriş dosyasını içeren dizinin mutlak yoludur - `%tempDir%` geçici dosyalar için dizinin mutlak yoludur - `%vendorDir%` Composer'ın kütüphaneleri yüklediği dizinin mutlak yoludur +- `%rootDir%` projenin kök dizinine giden mutlak yoldur - `%debugMode%` uygulamanın hata ayıklama modunda olup olmadığını gösterir - `%consoleMode%` isteğin komut satırı üzerinden gelip gelmediğini gösterir @@ -225,7 +268,7 @@ services: Yeni bir örnek oluşturun ve bootstrap'e ekleyin: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Farklı Ortamlar .[#toc-different-environments] ============================================== -`Bootstrap` sınıfını ihtiyaçlarınıza göre özelleştirmekten çekinmeyin. Web projelerini farklılaştırmak için `boot()` yöntemine parametreler ekleyebilir veya birim testleri için ortamı başlatan `bootForTests()`, komut satırından çağrılan komut dosyaları için `bootForCli()` gibi başka yöntemler ekleyebilirsiniz. + `Bootstrap` sınıfını ihtiyaçlarınıza göre özelleştirmekten çekinmeyin. Web projeleri arasında ayrım yapmak için `bootWebApplication()` yöntemine parametreler ekleyebilirsiniz. Alternatif olarak, birim testleri için ortamı başlatmak üzere `bootTestEnvironment()`, komut satırından çağrılan betikler için `bootConsoleApplication()` gibi başka yöntemler de ekleyebilirsiniz. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container +{ + Tester\Environment::setup(); // Nette Tester başlatma + $this->setupContainer(); + return $this->configurator->createContainer(); +} + +public function bootConsoleApplication(): Nette\DI\Container { - $configurator = self::boot(); - Tester\Environment::setup(); // Nette Tester initialization - return $configurator; + $this->configurator->setDebugMode(false); + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); } ``` diff --git a/application/tr/components.texy b/application/tr/components.texy index c0f7bd4c4a..cae382b8da 100644 --- a/application/tr/components.texy +++ b/application/tr/components.texy @@ -230,6 +230,28 @@ $this->redirect(/* ... */); // ve yeniden yönlendir ``` +Sinyalden Sonra Yeniden Yönlendirme .[#toc-redirection-after-a-signal] +====================================================================== + +Bir bileşen sinyali işlendikten sonra genellikle yeniden yönlendirme yapılır. Bu durum formlara benzer - bir form gönderildikten sonra, sayfa tarayıcıda yenilendiğinde verilerin yeniden gönderilmesini önlemek için de yönlendirme yaparız. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Bir bileşen yeniden kullanılabilir bir öğe olduğundan ve genellikle belirli sunuculara doğrudan bağımlı olmaması gerektiğinden, `redirect()` ve `link()` yöntemleri parametreyi otomatik olarak bir bileşen sinyali olarak yorumlar: + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +Farklı bir sunum yapan kişiye veya eyleme yönlendirmeniz gerekiyorsa, bunu sunum yapan kişi aracılığıyla yapabilirsiniz: + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Kalıcı Parametreler .[#toc-persistent-parameters] ================================================= @@ -347,7 +369,7 @@ services: Son olarak, bu fabrikayı sunucumuzda kullanacağız: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ Derinlemesine Bileşenler .[#toc-components-in-depth] Bir Nette Uygulamasındaki bileşenler, bu bölümün konusu olan sayfalara gömdüğümüz bir web uygulamasının yeniden kullanılabilir parçalarıdır. Böyle bir bileşenin yetenekleri tam olarak nelerdir? 1) bir şablon içinde oluşturulabilir -2) [AJAX isteği |ajax#invalidation] sırasında hangi bölümünün işleneceğini bilir (snippet'ler) +2) AJAX isteği sırasında [hangi bölümünün |ajax#snippets] işleneceğini bilir (snippet'ler) 3) durumunu bir URL'de saklama yeteneğine sahiptir (kalıcı parametreler) 4) kullanıcı eylemlerine (sinyallere) yanıt verme yeteneğine sahiptir 5) hiyerarşik bir yapı oluşturur (kökün sunum yapan kişi olduğu) diff --git a/application/tr/configuration.texy b/application/tr/configuration.texy index 8949e7d7e8..78f85dc3e2 100644 --- a/application/tr/configuration.texy +++ b/application/tr/configuration.texy @@ -13,11 +13,15 @@ application: # Tracy BlueScreen'de "Nette Application" panelini gösterir? debugger: ... # (bool) varsayılan olarak true - # error-presenter hata durumunda çağrılacak mı? - catchExceptions: ... # (bool) üretim modunda varsayılan olarak true + # error-presenter hata üzerine çağrılacak mı? + # sadece geliştirici modunda etkilidir + catchExceptions: ... # (bool) varsayılan olarak true # hata sunucusunun adı - errorPresenter: Error # (string) varsayılan olarak 'Nette:Error' + errorPresenter: Error # (string|array) varsayılan olarak 'Nette:Error' + + # sunum yapanlar ve etkinlikler için takma adlar tanımlar + aliases: ... # sunum yapan kişinin adını bir sınıfa çözümlemek için kuralları tanımlar mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) varsayılan değer false ``` -Hata sunucuları geliştirme modunda varsayılan olarak çağrılmadığından ve hatalar Tracy tarafından görüntülendiğinden, `catchExceptions` değerini `true` olarak değiştirmek, hata sunucularının geliştirme sırasında doğru çalıştığını doğrulamaya yardımcı olur. +`nette/application` sürüm 3.2'den itibaren bir çift hata sunucusu tanımlamak mümkündür: + +```neon +application: + errorPresenter: + 4xx: Error4xx # Nette\Application\BadRequestException için + 5xx: Error5xx # diğer istisnalar için +``` `silentLinks` seçeneği, bağlantı oluşturma başarısız olduğunda (örneğin, sunum yapan kişi olmadığından vb.) Nette'in geliştirici modunda nasıl davranacağını belirler. Varsayılan değer olan `false`, Nette'in `E_USER_WARNING` adresini tetikleyeceği anlamına gelir. `true` olarak ayarlanması bu hata mesajını bastırır. Bir üretim ortamında, `E_USER_WARNING` her zaman çağrılır. Bu davranışı, sunum yapan değişken [$invalidLinkMode'u |creating-links#Invalid Links] ayarlayarak da etkileyebiliriz. -[Eşleme |modules#mapping], sınıf adının sunum yapan kişinin adından türetildiği [kuralları tanımlar |modules#mapping]. +[Takma adlar |creating-links#aliases], sık kullanılan sunum yapanlara [atıfta bulunmayı kolaylaştırır |creating-links#aliases]. + +[Eşleme |directory-structure#Presenter Mapping], sınıf adının sunum yapan kişinin adından türetildiği [kuralları tanımlar |directory-structure#Presenter Mapping]. Sunum Yapanların Otomatik Kaydı .[#toc-automatic-registration-of-presenters] @@ -82,6 +95,9 @@ latte: # [oluşturulan kodun kontrol |latte:develop#Checking Generated Code]edilmesini sağlar phpLinter: ... # (string) varsayılan null + # yerel ayarı ayarlar + locale: cs_CZ # (string) varsayılan null + # $this->template sınıfı templateClass: App\MyTemplateClass # varsayılan olarak Nette\Bridges\ApplicationLatte\DefaultTemplate ``` @@ -91,7 +107,7 @@ Latte sürüm 3 kullanıyorsanız, kullanarak yeni [uzantı |latte:creating-exte ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` /--comment diff --git a/application/tr/creating-links.texy b/application/tr/creating-links.texy index 1b8325a446..f1b9985a1d 100644 --- a/application/tr/creating-links.texy +++ b/application/tr/creating-links.texy @@ -38,7 +38,7 @@ Adlandırılmış parametreleri geçmek de mümkündür. Aşağıdaki bağlantı detail ``` -`ProductPresenter::renderShow()` yönteminin imzasında `$lang` yoksa, parametrenin değerini `$lang = $this->getParameter('lang')` kullanarak okuyabilir. +`ProductPresenter::renderShow()` yönteminin imzasında `$lang` yoksa, parametrenin değerini `$lang = $this->getParameter('lang')` kullanarak veya [özellikten |presenters#Request Parameters] alabilir. Parametreler bir dizide saklanıyorsa, `...` operatörü (veya Latte 2.x'te `(expand)` operatörü) ile genişletilebilirler: @@ -103,7 +103,7 @@ Eylem `default` ise, bunu atlayabiliriz, ancak iki nokta üst üste kalmalıdır home ``` -Bağlantılar diğer [modüllere |modules] de işaret edebilir. Burada, bağlantılar alt modüllere göreli veya mutlak olarak ayırt edilir. Prensip disk yollarına benzer, sadece eğik çizgiler yerine iki nokta üst üste vardır. Gerçek sunucunun `Front` modülünün bir parçası olduğunu varsayalım, o zaman yazacağız: +Bağlantılar diğer [modüllere |directory-structure#Presenters and Templates] de işaret edebilir. Burada bağlantılar alt modüllere göre göreceli veya mutlak olarak ayrılır. Prensip disk yollarına benzer, sadece eğik çizgiler yerine iki nokta üst üste vardır. Gerçek sunucunun `Front` modülünün bir parçası olduğunu varsayalım, o zaman yazacağız: ```latte link to Front:Shop:Product:show @@ -140,7 +140,7 @@ Hedef `this` geçerli sayfaya bir bağlantı oluşturacaktır: refresh ``` -Aynı zamanda, imzasında belirtilen tüm parametreler `render()` veya `action()` yöntemi aktarılır. Yani `Product:show` ve `id:123` sayfalarındaysak, `this` bağlantısı da bu parametreyi geçirecektir. +Aynı zamanda, imzasında belirtilen tüm parametreler `action()` veya `render()` yönteminde, eğer `action()` tanımlanmamışsa, aktarılır. Yani `Product:show` ve `id:123` sayfalarındaysak, `this` bağlantısı da bu parametreyi geçirecektir. Elbette parametreleri doğrudan belirtmek de mümkündür: @@ -213,7 +213,7 @@ Bileşendeki Bağlantılar .[#toc-links-in-component] Bileşen şablonunda sunum yapanlara bağlantı vermek istiyorsak `{plink}` etiketini kullanırız: ```latte -home +home ``` veya kodda @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Takma Adlar .[#toc-aliases]{data-version:v3.2.2} +================================================ + +Bazen bir Presenter:action çiftine kolayca hatırlanabilir bir takma ad atamak yararlı olabilir. Örneğin, `Front:Home:default` ana sayfasını basitçe `home` veya `Admin:Dashboard:default` ana sayfasını `admin` olarak adlandırabilirsiniz. + +Takma adlar [yapılandırmada |configuration] `application › aliases` anahtarı altında tanımlanır: + +```neon +application: + aliases: + home: Front:Home:default + admin: Admin:Dashboard:default + sign: Front:Sign:in +``` + +Bağlantılarda, örneğin at sembolü kullanılarak yazılırlar: + +```latte +administration +``` + + `redirect()` ve benzeri gibi bağlantılarla çalışan tüm yöntemlerde desteklenirler. + + Geçersiz Bağlantılar .[#toc-invalid-links] ========================================== @@ -257,6 +281,6 @@ LinkGenerator .[#toc-linkgenerator] LinkGenerator, kurucudan geçirebileceğiniz ve ardından `link()` yöntemini kullanarak bağlantılar oluşturabileceğiniz bir hizmettir. -Sunuculara kıyasla bir fark vardır. LinkGenerator tüm bağlantıları mutlak URL'ler olarak oluşturur. Ayrıca, "geçerli sunucu" yoktur, bu nedenle yalnızca `link('default')` eyleminin adını veya [modüllere |modules] göreli yolları belirtmek mümkün değildir. +Sunucularla karşılaştırıldığında bir fark var. LinkGenerator tüm bağlantıları doğrudan mutlak URL'ler olarak oluşturur. Ayrıca, "gerçek sunucu" yoktur, bu nedenle `link('default')` eylem adını hedef olarak listeleyemez veya modüllere göreli yolları listeleyemezsiniz. Geçersiz bağlantılar her zaman `Nette\Application\UI\InvalidLinkException` adresini atar. diff --git a/application/tr/directory-structure.texy b/application/tr/directory-structure.texy new file mode 100644 index 0000000000..49d8805fd7 --- /dev/null +++ b/application/tr/directory-structure.texy @@ -0,0 +1,526 @@ +Uygulamanın Dizin Yapısı +************************ + +
    + +Nette Framework'te projeler için net ve ölçeklenebilir bir dizin yapısı nasıl tasarlanır? Kodunuzu düzenlemenize yardımcı olacak kanıtlanmış uygulamaları göstereceğiz. Öğreneceksiniz: + +- uygulamanın dizinler halinde **mantıksal olarak** nasıl yapılandırılacağı +- Proje büyüdükçe yapının **iyi ölçeklendirilecek** şekilde nasıl tasarlanacağı +- olası alternatifler** ve bunların avantajları ya da dezavantajları nelerdir? + +
    + + +Nette Framework'ün kendisinin herhangi bir özel yapıda ısrar etmediğini belirtmek önemlidir. Her türlü ihtiyaç ve tercihe kolayca uyarlanabilecek şekilde tasarlanmıştır. + + +Temel Proje Yapısı .[#toc-basic-project-structure] +================================================== + +Nette Framework herhangi bir sabit dizin yapısı dikte etmese de, [Web Projesi |https://github.com/nette/web-project] şeklinde kanıtlanmış bir varsayılan düzenleme vardır: + +/--pre +web-project/ +├── app/ ← uygulama dizini +├── assets/ ← SCSS, JS dosyaları, resimler..., alternatif olarak kaynaklar/ +├── bin/ ← komut satırı komut dosyaları +├── config/ ← yapılandırma +├── log/ ← günlüğe kaydedilen hatalar +├── temp/ ← geçici dosyalar, önbellek +├── tests/ ← testler +├── vendor/ ← Composer tarafından yüklenen kütüphaneler +└── www/ ← genel dizin (document-root) +\-- + +Bu yapıyı ihtiyaçlarınıza göre serbestçe değiştirebilirsiniz - klasörleri yeniden adlandırabilir veya taşıyabilirsiniz. O zaman sadece `Bootstrap.php` ve muhtemelen `composer.json` adresindeki dizinlerin göreli yollarını ayarlamanız gerekir. Başka hiçbir şeye gerek yok, karmaşık yeniden yapılandırma yok, sürekli değişiklik yok. Nette akıllı otomatik algılamaya sahiptir ve URL tabanı da dahil olmak üzere uygulama konumunu otomatik olarak tanır. + + +Kod Organizasyon İlkeleri .[#toc-code-organization-principles] +============================================================== + +Yeni bir projeyi ilk keşfettiğinizde, kendinizi hızlı bir şekilde yönlendirebilmelisiniz. `app/Model/` dizinine tıkladığınızı ve bu yapıyı gördüğünüzü hayal edin: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +Buradan yalnızca projenin bazı hizmetleri, depoları ve varlıkları kullandığını öğreneceksiniz. Başvurunun asıl amacı hakkında hiçbir şey öğrenemezsiniz. + +Şimdi farklı bir yaklaşıma bakalım - **alanlara göre düzenleme**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Bu farklı - ilk bakışta bunun bir e-ticaret sitesi olduğu açık. Dizin adları, uygulamanın ne yapabileceğini ortaya koyuyor - ödemeler, siparişler ve ürünlerle çalışıyor. + +İlk yaklaşım (sınıf türüne göre düzenleme) pratikte çeşitli sorunlara yol açar: mantıksal olarak ilişkili olan kod farklı klasörlere dağılmıştır ve bunlar arasında geçiş yapmanız gerekir. Bu nedenle, alanlara göre düzenleyeceğiz. + + +İsim Alanları .[#toc-namespaces] +-------------------------------- + +Dizin yapısının uygulamadaki isim alanlarına karşılık gelmesi gelenekseldir. Bu, dosyaların fiziksel konumlarının isim alanlarına karşılık geldiği anlamına gelir. Örneğin, `app/Model/Product/ProductRepository.php` adresinde bulunan bir sınıf `App\Model\Product` ad alanına sahip olmalıdır. Bu ilke, kod oryantasyonuna yardımcı olur ve otomatik yüklemeyi basitleştirir. + + +İsimlerde Tekil ve Çoğul .[#toc-singular-vs-plural-in-names] +------------------------------------------------------------ + +Ana uygulama dizinleri için tekil kullandığımıza dikkat edin: `app`, `config`, `log`, `temp`, `www`. Aynı şey uygulama içinde de geçerlidir: `Model`, `Core`, `Presentation`. Bunun nedeni, her birinin birleşik bir kavramı temsil etmesidir. + +Benzer şekilde, `app/Model/Product` ürünler hakkında her şeyi temsil eder. Buna `Products` demiyoruz çünkü ürünlerle dolu bir klasör değil ( `iphone.php`, `samsung.php` gibi dosyalar içerirdi). Ürünlerle çalışmak için sınıflar içeren bir ad alanıdır - `ProductRepository.php`, `ProductService.php`. + + `app/Tasks` klasörü çoğuldur çünkü bir dizi bağımsız çalıştırılabilir komut dosyası içerir - `CleanupTask.php`, `ImportTask.php`. Her biri bağımsız bir birimdir. + +Tutarlılık için kullanmanızı öneririz: +- Bir işlevsel birimi temsil eden ad alanları için tekil (birden fazla varlık ile çalışılsa bile) +- Bağımsız birim koleksiyonları için çoğul +- Belirsizlik durumunda veya bu konuda düşünmek istemiyorsanız, tekil seçin + + +Genel Rehber `www/` .[#toc-public-directory-www] +================================================ + +Bu dizin web'den erişilebilen tek dizindir (belge kökü olarak adlandırılır). Sık sık `www/` yerine `public/` adıyla karşılaşabilirsiniz - bu sadece bir kural meselesidir ve işlevselliği etkilemez. Dizin şunları içerir: +- Uygulama [giriş noktası |bootstrap#index.php] `index.php` +- `.htaccess` mod_rewrite kurallarını içeren dosya (Apache için) +- Statik dosyalar (CSS, JavaScript, resimler) +- Yüklenen dosyalar + +Doğru uygulama güvenliği için [document-root |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url]'un doğru [yapılandırılmış |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url] olması çok önemlidir. + +.[note] + `node_modules/` klasörünü asla bu dizine yerleştirmeyin - çalıştırılabilir olabilecek ve herkesin erişimine açık olmaması gereken binlerce dosya içerir. + + +Uygulama Dizini `app/` .[#toc-application-directory-app] +======================================================== + +Bu, uygulama kodunun bulunduğu ana dizindir. Temel yapı: + +/--pre +app/ +├── Core/ ← altyapi konulari +├── Model/ ← iş mantığı +├── Presentation/ ← sunum yapan ki̇şi̇ler ve şablonlar +├── Tasks/ ← komut dosyaları +└── Bootstrap.php ← uygulama önyükleme sınıfı +\-- + +`Bootstrap.php` ortamı başlatan, yapılandırmayı yükleyen ve DI konteynerini oluşturan [uygulama başlangıç sınıfıdır |bootstrap]. + +Şimdi tek tek alt dizinlere ayrıntılı olarak bakalım. + + +Sunucular ve Şablonlar .[#toc-presenters-and-templates] +======================================================= + +Uygulamanın sunum kısmını `app/Presentation` dizininde bulunduruyoruz. Bir alternatif de kısa `app/UI`. Burası tüm sunum yapanların, şablonlarının ve yardımcı sınıfların yeridir. + +Bu katmanı etki alanlarına göre düzenliyoruz. E-ticaret, blog ve API'yi birleştiren karmaşık bir projede, yapı şu şekilde görünecektir: + +/--pre +app/Presentation/ +├── Shop/ ← e-ticaret ön yüzü +│ ├── Product/ +│ ├── Cart/ +│ └── Order/ +├── Blog/ ← blog +│ ├── Home/ +│ └── Post/ +├── Admin/ ← YÖNETİM +│ ├── Dashboard/ +│ └── Products/ +└── Api/ ← API uç noktaları + └── V1/ +\-- + +Tersine, basit bir blog için bu yapıyı kullanırız: + +/--pre +app/Presentation/ +├── Front/ ← web sitesi ön yüzü +│ ├── Home/ +│ └── Post/ +├── Admin/ ← YÖNETİM +│ ├── Dashboard/ +│ └── Posts/ +├── Error/ +└── Export/ ← RSS, site haritaları vb. +\-- + + `Home/` veya `Dashboard/` gibi klasörler sunum yapan kişileri ve şablonları içerir. `Front/` , `Admin/` veya `Api/` gibi klasörler **modüller** olarak adlandırılır. Teknik olarak, bunlar uygulamanın mantıksal organizasyonuna hizmet eden normal dizinlerdir. + +Sunum yapan kişinin bulunduğu her klasör, benzer şekilde adlandırılmış bir sunum yapan kişi ve onun şablonlarını içerir. Örneğin, `Dashboard/` klasörü şunları içerir: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← SUNUCU +└── default.latte ← şablon +\-- + +Bu dizin yapısı sınıf ad alanlarına yansıtılır. Örneğin, `DashboardPresenter`, `App\Presentation\Admin\Dashboard` ad alanındadır (bkz. [sunucu eşleme |#presenter mapping]): + +```php +namespace App\Presentation\Admin\Dashboard; + +class DashboardPresenter extends Nette\Application\UI\Presenter +{ + //... +} +``` + +Uygulamadaki `Admin` modülünün içindeki `Dashboard` sunumcusuna iki nokta üst üste gösterimini kullanarak `Admin:Dashboard` şeklinde atıfta bulunuyoruz. `default` eylemine daha sonra `Admin:Dashboard:default` olarak. İç içe modüller için daha fazla iki nokta üst üste kullanırız, örneğin `Shop:Order:Detail:default`. + + +Esnek Yapı Geliştirme .[#toc-flexible-structure-development] +------------------------------------------------------------ + +Bu yapının en büyük avantajlarından biri, büyüyen proje ihtiyaçlarına ne kadar zarif bir şekilde uyum sağlamasıdır. Örnek olarak, XML beslemeleri üreten bölümü ele alalım. Başlangıçta basit bir formumuz var: + +/--pre +Export/ +├── ExportPresenter.php ← tüm ihracatlar için bir sunumcu +├── sitemap.latte ← site haritası için şablon +└── feed.latte ← RSS beslemesi için şablon +\-- + +Zamanla daha fazla besleme türü eklenir ve bunlar için daha fazla mantığa ihtiyacımız olur... Sorun değil! `Export/` klasörü basitçe bir modül haline gelir: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← Amazon için besleme + └── ebay.latte ← eBay için besleme +\-- + +Bu dönüşüm tamamen sorunsuzdur - sadece yeni alt klasörler oluşturun, kodu bunlara bölün ve bağlantıları güncelleyin (örneğin `Export:feed` adresinden `Export:Feed:amazon` adresine). Bu sayede yapıyı gerektiği gibi kademeli olarak genişletebiliriz, yuvalama seviyesi hiçbir şekilde sınırlı değildir. + +Örneğin, yönetimde `OrderDetail`, `OrderEdit`, `OrderDispatch` vb. gibi sipariş yönetimi ile ilgili birçok sunumcunuz varsa, daha iyi organizasyon için `Detail`, `Edit`, `Dispatch` ve diğer sunumcuları (klasörleri) içeren bir modül (klasör) `Order` oluşturabilirsiniz. + + +Şablon Konumu .[#toc-template-location] +--------------------------------------- + +Önceki örneklerde, şablonların doğrudan sunum yapan kişinin bulunduğu klasörde yer aldığını görmüştük: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← SUNUCU +├── DashboardTemplate.php ← isteğe bağlı şablon sınıfı +└── default.latte ← şablon +\-- + +Bu konum pratikte en uygun konumdur - ilgili tüm dosyalar elinizin altındadır. + +Alternatif olarak, şablonları bir `templates/` alt klasörüne yerleştirebilirsiniz. Nette her iki seçeneği de destekler. Hatta şablonları `Presentation/` klasörünün tamamen dışına da yerleştirebilirsiniz. [Şablon |templates#Template Lookup] konumu seçenekleriyle ilgili her şeyi [Şablon Arama |templates#Template Lookup] bölümünde bulabilirsiniz. + + +Yardımcı Sınıflar ve Bileşenler .[#toc-helper-classes-and-components] +--------------------------------------------------------------------- + +Sunucular ve şablonlar genellikle başka yardımcı dosyalarla birlikte gelir. Bunları kapsamlarına göre mantıksal olarak yerleştiriyoruz: + +1. Verilen sunum yapan kişi için özel bileşenler olması durumunda **Doğrudan sunum yapan kişiyle**: + +/--pre +Product/ +├── ProductPresenter.php +├── ProductGrid.php ← ürün listeleme için bileşen +└── FilterForm.php ← filtreleme için form +\-- + +2. **Modül** için - alfabenin başına düzgün bir şekilde yerleştirilmiş olan `Accessory` klasörünü kullanmanızı öneririz: + +/--pre +Front/ +├── Accessory/ +│ ├── NavbarControl.php ← ön uç için bileşenler +│ └── TemplateFilters.php +├── Product/ +└── Cart/ +\-- + +3. **Başvurunun tamamı için** - `Presentation/Accessory/` adresinde : +/--pre +app/Presentation/ +├── Accessory/ +│ ├── LatteExtension.php +│ └── TemplateFilters.php +├── Front/ +└── Admin/ +\-- + +Ya da `LatteExtension.php` veya `TemplateFilters.php` gibi yardımcı sınıfları `app/Core/Latte/` altyapı klasörüne yerleştirebilirsiniz. Ve bileşenleri `app/Components`. Seçim takım geleneklerine bağlıdır. + + +Model - Uygulamanın Kalbi .[#toc-model-heart-of-the-application] +================================================================ + +Model, uygulamanın tüm iş mantığını içerir. Organizasyonu için de aynı kural geçerlidir - etki alanlarına göre yapılandırırız: + +/--pre +app/Model/ +├── Payment/ ← ödemeler hakkında her şey +│ ├── PaymentFacade.php ← ana giriş noktası +│ ├── PaymentRepository.php +│ ├── Payment.php ← varlık +├── Order/ ← si̇pari̇şler hakkinda her şey +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← nakli̇yat hakkinda her şey +\-- + +Modelde genellikle bu tür sınıflarla karşılaşırsınız: + +**Cephe**: uygulamadaki belirli bir alana ana giriş noktasını temsil eder. Tam kullanım durumlarını ("sipariş oluşturma" veya "ödeme işleme" gibi) uygulamak için farklı hizmetler arasındaki işbirliğini koordine eden bir orkestratör görevi görürler. Orkestrasyon katmanı altında, cephe uygulama ayrıntılarını uygulamanın geri kalanından gizler, böylece verilen alanla çalışmak için temiz bir arayüz sağlar. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // doğrulama + // sipariş oluşturma + // e-posta gönderme + // i̇stati̇sti̇klere yazma + } +} +``` + +**Hizmetler**: bir etki alanı içindeki belirli iş operasyonlarına odaklanır. Tüm kullanım durumlarını düzenleyen fasadların aksine, bir hizmet belirli iş mantığını uygular (fiyat hesaplamaları veya ödeme işleme gibi). Hizmetler genellikle durum bilgisi içermez ve daha karmaşık işlemler için yapı taşları olarak cepheler tarafından ya da daha basit görevler için doğrudan uygulamanın diğer bölümleri tarafından kullanılabilir. + +```php +class PricingService +{ + public function calculateTotal(Order $order): Money + { + // fi̇yat hesaplama + } +} +``` + +**Depolar**: tipik olarak bir veritabanı olan veri depolama ile tüm iletişimi gerçekleştirir. Görevleri varlıkları yüklemek ve kaydetmek ve bunları aramak için yöntemler uygulamaktır. Depo, uygulamanın geri kalanını veritabanı uygulama ayrıntılarından korur ve verilerle çalışmak için nesne yönelimli bir arayüz sağlar. + +```php +class OrderRepository +{ + public function find(int $id): ?Order + { + } + + public function findByCustomer(int $customerId): array + { + } +} +``` + +**Varlıklar**: uygulamadaki ana iş kavramlarını temsil eden, kimlikleri olan ve zaman içinde değişen nesneler. Tipik olarak bunlar ORM (Nette Database Explorer veya Doctrine gibi) kullanılarak veritabanı tablolarına eşlenen sınıflardır. Varlıklar, verileri ve doğrulama mantıklarıyla ilgili iş kuralları içerebilir. + +```php +// Veritabanı tablosu siparişleriyle eşlenen varlık +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, + ]); + } +} +``` + +**Değer nesneleri**: kendi kimlikleri olmayan değerleri temsil eden değişmez nesneler - örneğin, bir para miktarı veya e-posta adresi. Bir değer nesnesinin aynı değerlere sahip iki örneği aynı kabul edilir. + + +Altyapı Kodu .[#toc-infrastructure-code] +======================================== + + `Core/` klasörü (veya ayrıca `Infrastructure/`) uygulamanın teknik temeline ev sahipliği yapar. Altyapı kodu tipik olarak şunları içerir: + +/--pre +app/Core/ +├── Router/ ← yönlendirme ve URL yönetimi +│ └── RouterFactory.php +├── Security/ ← kimlik doğrulama ve yetkilendirme +│ ├── Authenticator.php +│ └── Authorizator.php +├── Logging/ ← günlüğe kaydetme ve izleme +│ ├── SentryLogger.php +│ └── FileLogger.php +├── Cache/ ← önbellekleme katmanı +│ └── FullPageCache.php +└── Integration/ ← harici hizmetlerle entegrasyon + ├── Slack/ + └── Stripe/ +\-- + +Daha küçük projeler için düz bir yapı doğal olarak yeterlidir: + +/--pre +Core/ +├── RouterFactory.php +├── Authenticator.php +└── QueueMailer.php +\-- + +Bu bir kod: + +- Teknik altyapıyı yönetir (yönlendirme, günlüğe kaydetme, önbelleğe alma) +- Harici hizmetleri entegre eder (Sentry, Elasticsearch, Redis) +- Tüm uygulama için temel hizmetleri sağlar (posta, veritabanı) +- Çoğunlukla belirli bir etki alanından bağımsızdır - önbellek veya kaydedici, e-ticaret veya blog için aynı şekilde çalışır. + +Belirli bir sınıfın buraya mı yoksa modele mi ait olduğunu merak ediyor musunuz? Temel fark, `Core/` adresindeki koddur: + +- Alan hakkında hiçbir şey bilmiyor (ürünler, siparişler, makaleler) +- Genellikle başka bir projeye aktarılabilir +- "Nasıl çalıştığını" (nasıl posta gönderileceğini) çözer, "ne yaptığını" (hangi postanın gönderileceğini) değil + +Daha iyi anlaşılması için örnek: + +- `App\Core\MailerFactory` - e-posta gönderme sınıfının örneklerini oluşturur, SMTP ayarlarını yapar +- `App\Model\OrderMailer` - siparişler hakkında e-posta göndermek için `MailerFactory` adresini kullanır, şablonlarını ve ne zaman gönderilmeleri gerektiğini bilir + + +Komut Komut Dosyaları .[#toc-command-scripts] +============================================= + +Uygulamalar genellikle arka planda veri işleme, bakım veya periyodik görevler gibi normal HTTP istekleri dışında görevler gerçekleştirmeye ihtiyaç duyar. Gerçek uygulama mantığı `app/Tasks/` (veya `app/Commands/`) dizinine yerleştirilirken, `bin/` dizinindeki basit komut dosyaları yürütme için kullanılır. + +Örnek: + +/--pre +app/Tasks/ +├── Maintenance/ ← bakım komut dosyaları +│ ├── CleanupCommand.php ← eski verileri silme +│ └── DbOptimizeCommand.php ← veritabanı optimizasyonu +├── Integration/ ← harici sistemlerle entegrasyon +│ ├── ImportProducts.php ← tedarikçi sisteminden içe aktar +│ └── SyncOrders.php ← sipariş senkronizasyonu +└── Scheduled/ ← düzenli görevler + ├── NewsletterCommand.php ← haber bültenleri gönderme + └── ReminderCommand.php ← müşteri̇ bi̇ldi̇ri̇mleri̇ +\-- + +Ne modele ne de komut dosyalarına aittir? Örneğin, bir e-posta gönderme mantığı modelin bir parçasıdır, binlerce e-postanın toplu olarak gönderilmesi `Tasks/` adresine aittir. + +Görevler genellikle [komut satırından |https://blog.nette.org/en/cli-scripts-in-nette-application] veya cron aracılığıyla [çalıştırılır |https://blog.nette.org/en/cli-scripts-in-nette-application]. HTTP isteği yoluyla da çalıştırılabilirler, ancak güvenlik göz önünde bulundurulmalıdır. Görevi çalıştıran sunucunun, örneğin yalnızca oturum açmış kullanıcılar için veya güçlü bir belirteçle ve izin verilen IP adreslerinden erişimle güvenli hale getirilmesi gerekir. Uzun görevler için komut dosyası zaman sınırını artırmak ve oturumun kilitlenmesini önlemek için `session_write_close()` adresini kullanmak gerekir. + + +Diğer Olası Dizinler .[#toc-other-possible-directories] +======================================================= + +Bahsedilen temel dizinlere ek olarak, proje ihtiyaçlarına göre başka özel klasörler de ekleyebilirsiniz. Şimdi en yaygın olanlara ve kullanımlarına bakalım: + +/--pre +app/ +├── Api/ ← Sunum katmanından bağımsız API mantığı +├── Database/ ← test verileri için geçiş komut dosyaları ve tohumlayıcılar +├── Components/ ← uygulama genelinde paylaşılan görsel bileşenler +├── Event/ ← olay güdümlü mimari kullanılıyorsa kullanışlıdır +├── Mail/ ← e-posta şablonları ve ilgili mantık +└── Utils/ ← yardımcı sınıflar +\-- + +Uygulama genelinde sunumlarda kullanılan paylaşılan görsel bileşenler için `app/Components` veya `app/Controls` klasörünü kullanabilirsiniz: + +/--pre +app/Components/ +├── Form/ ← paylaşılan form bileşenleri +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← veri listeleri için bileşenler +│ └── DataGrid.php +└── Navigation/ ← navigasyon öğeleri + ├── Breadcrumbs.php + └── Menu.php +\-- + +Burası daha karmaşık mantığa sahip bileşenlerin ait olduğu yerdir. Bileşenleri birden fazla proje arasında paylaşmak istiyorsanız, bunları bağımsız bir composer paketine ayırmanız iyi olur. + + `app/Mail` dizinine e-posta iletişim yönetimini yerleştirebilirsiniz: + +/--pre +app/Mail/ +├── templates/ ← e-posta şablonları +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Sunucu Haritalama .[#toc-presenter-mapping] +=========================================== + +Eşleme, sınıf adlarının sunucu adlarından türetilmesi için kuralları tanımlar. Bunları [yapılandırmada |configuration] `application › mapping` anahtarı altında belirtiriz. + +Bu sayfada, sunum yapan kişileri `app/Presentation` klasörüne (veya `app/UI`) yerleştirdiğimizi gösterdik. Nette'e yapılandırma dosyasında bu konvansiyon hakkında bilgi vermemiz gerekiyor. Bir satır yeterli: + +```neon +application: + mapping: App\Presentation\*\**Presenter +``` + +Eşleme nasıl çalışır? Daha iyi anlamak için öncelikle modülsüz bir uygulama hayal edelim. Sunucu sınıflarının `App\Presentation` ad alanı altında olmasını istiyoruz, böylece `Home` sunucusu `App\Presentation\HomePresenter` sınıfıyla eşleşir. Bu, bu yapılandırma ile elde edilir: + +```neon +application: + mapping: App\Presentation\*Presenter +``` + +Eşleme, `App\Presentation\*Presenter` maskesindeki yıldız işaretini `Home` sunum yapan kişi adıyla değiştirerek çalışır ve sonuçta `App\Presentation\HomePresenter` nihai sınıf adı elde edilir. Çok basit! + +Ancak, bu ve diğer bölümlerdeki örneklerde gördüğünüz gibi, sunum sınıflarını isimsiz alt dizinlere yerleştiririz, örneğin `Home` sunum sınıfı `App\Presentation\Home\HomePresenter` sınıfına eşlenir. Bunu iki nokta üst üste işaretini iki katına çıkararak başarıyoruz (Nette Application 3.2 gerektirir): + +```neon +application: + mapping: App\Presentation\**Presenter +``` + +Şimdi sunum yapan kişileri modüllerle eşleştirmeye geçeceğiz. Her modül için özel eşleme tanımlayabiliriz: + +```neon +application: + mapping: + Front: App\Presentation\Front\**Presenter + Admin: App\Presentation\Admin\**Presenter + Api: App\Api\*Presenter +``` + +Bu yapılandırmaya göre, `Front:Home` sunucusu `App\Presentation\Front\Home\HomePresenter` sınıfıyla eşleşirken, `Api:OAuth` sunucusu `App\Api\OAuthPresenter` sınıfıyla eşleşir. + + `Front` ve `Admin` modülleri benzer bir eşleme yöntemine sahip olduğundan ve muhtemelen bu tür daha fazla modül olacağından, bunların yerini alacak genel bir kural oluşturmak mümkündür. Sınıf maskesine modül için yeni bir yıldız işareti eklenecektir: + +```neon +application: + mapping: + *: App\Presentation\*\**Presenter + Api: App\Api\*Presenter +``` + +Ayrıca, yıldız işaretli bölümün her seviye için tekrarlandığı ve `App\Presentation\Admin\User\Edit\EditPresenter` sınıfıyla sonuçlandığı `Admin:User:Edit` gibi daha derin iç içe dizin yapıları için de çalışır. + +Alternatif bir gösterim, bir dize yerine üç segmentten oluşan bir dizi kullanmaktır. Bu gösterim bir öncekine eşdeğerdir: + +```neon +application: + mapping: + *: [App\Presentation, *, **Presenter] + Api: [App\Api, '', *Presenter] +``` diff --git a/application/tr/how-it-works.texy b/application/tr/how-it-works.texy index c083b0f45c..91138083a6 100644 --- a/application/tr/how-it-works.texy +++ b/application/tr/how-it-works.texy @@ -22,18 +22,18 @@ Dizin yapısı şuna benzer: /--pre web-project/ ├── app/ ← directory with application -│ ├── Presenters/ ← presenter classes -│ │ ├── HomePresenter.php ← Home presenter class -│ │ └── templates/ ← templates directory -│ │ ├── @layout.latte ← template of shared layout -│ │ └── Home/ ← templates for Home presenter -│ │ └── default.latte ← template for action `default` -│ ├── Router/ ← configuration of URL addresses +│ ├── Core/ ← temel gerekli sınıflar +│ │ └── RouterFactory.php ← URL adreslerinin yapılandırılması +│ ├── Presentation/ ← presenters, templates & co. +│ │ ├── @layout.latte ← paylaşılan düzen şablonu +│ │ └── Home/ ← Ana Sayfa sunucu dizini +│ │ ├── HomePresenter.php ← Ev sunucusu sınıfı +│ │ └── default.latte ← eylem için şablon default │ └── Bootstrap.php ← booting class Bootstrap ├── bin/ ← scripts for the command line ├── config/ ← configuration files │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← error logs ├── temp/ ← temporary files, cache, … ├── vendor/ ← libraries installed by Composer @@ -45,9 +45,9 @@ Dizin yapısı şuna benzer: └── .htaccess ← prohibits access to all directories except www \-- -Dizin yapısını herhangi bir şekilde değiştirebilir, klasörleri yeniden adlandırabilir veya taşıyabilir ve ardından `Bootstrap.php` dosyasındaki `log/` ve `temp/` yollarını ve `autoload` bölümündeki `composer.json` içindeki bu dosyanın yolunu düzenleyebilirsiniz. Başka bir şey yok, karmaşık yeniden yapılandırma yok, sürekli değişiklik yok. Nette [akıllı |bootstrap#development-vs-production-mode] bir [otomatik algılamaya |bootstrap#development-vs-production-mode] sahiptir. +Dizin yapısını istediğiniz gibi değiştirebilir, klasörleri yeniden adlandırabilir veya taşıyabilirsiniz - tamamen esnektir. Nette ayrıca akıllı otomatik algılama özelliğine sahiptir ve URL tabanı da dahil olmak üzere uygulama konumunu otomatik olarak tanır. -Biraz daha büyük uygulamalar için, sunum ve şablon içeren klasörleri alt dizinlere (diskte) ve [modül |modules] olarak adlandırdığımız ad alanlarına (kodda) bölebiliriz. +Biraz daha büyük uygulamalar için, sunumcu ve şablon klasörlerini [alt dizinler |directory-structure#Presenters and templates] halinde düzenleyebilir ve sınıfları modül olarak adlandırdığımız ad alanlarında gruplayabiliriz. `www/` dizini, projenin genel dizini veya belge köküdür. Uygulama tarafında başka bir şey ayarlamanıza gerek kalmadan yeniden adlandırabilirsiniz. Sadece [barındırmayı |nette:troubleshooting#How to change or remove www directory from URL], belge kökünün bu dizine gideceği şekilde [yapılandırmanız |nette:troubleshooting#How to change or remove www directory from URL] gerekir. @@ -75,7 +75,7 @@ Görevi: Ne tür bir fabrika? Traktör değil, web sitesi üretiyoruz! Bekleyin, hemen açıklayacağım. -"Ortamı başlatmak" derken, örneğin, hataları günlüğe kaydetmek veya görselleştirmek için harika bir araç olan [Tracy |tracy:] 'nin etkinleştirilmesini kastediyoruz. Üretim sunucusundaki hataları günlüğe kaydeder ve bunları doğrudan geliştirme sunucusunda görüntüler. Bu nedenle, başlatmanın sitenin üretim modunda mı yoksa geliştirici modunda mı çalıştığına da karar vermesi gerekir. Bunu yapmak için Nette otomatik algılama kullanır: siteyi localhost üzerinde çalıştırırsanız, geliştirici modunda çalışır. Hiçbir şey yapılandırmanız gerekmez ve uygulama hem geliştirme hem de üretim dağıtımı için hazırdır. Bu adımlar [Bootstrap sınıfı |bootstrap] ile ilgili bölümde ayrıntılı olarak açıklanmıştır. +"Ortamın başlatılması" derken, örneğin, günlük kaydı ve hata görselleştirme için harika bir araç olan [Tracy |tracy:]'nin etkinleştirilmesini kastediyoruz. Üretim sunucularında hataları günlüğe kaydeder, geliştirme sunucularında ise doğrudan görüntüler. Bu nedenle, başlatma, web sitesinin üretim veya geliştirme modunda çalışıp çalışmadığını belirlemeyi içerir. Bunun için Nette [akıllı otomatik algılama |bootstrap#development-vs-production-mode] kullanır: siteyi localhost üzerinde çalıştırırsanız, geliştirme modunda çalışır. Yapılandırma gerekmez ve uygulama hem geliştirme hem de üretim dağıtımı için hazırdır. Bu adımlar [Bootstrap sınıfı |bootstrap] bölümünde gerçekleştirilir ve detaylandırılır. Üçüncü nokta (evet, ikinciyi atladık, ancak ona geri döneceğiz) uygulamayı başlatmaktır. Nette HTTP isteklerinin işlenmesi `Nette\Application\Application` sınıfı (bundan sonra `Application` olarak anılacaktır) tarafından yapılır, bu nedenle "bir uygulamayı çalıştır" dediğimizde, bu sınıfın bir nesnesi üzerinde `run()` adlı bir yöntemi çağırmayı kastediyoruz. @@ -91,7 +91,7 @@ Nette'de yazılan uygulamalar, belirli bir web sitesi sayfasını temsil eden s Uygulama, yönlendirici olarak adlandırılan kişiden mevcut talebin işlenmek üzere hangi sunuculara iletileceğine karar vermesini isteyerek başlar. Yönlendirici bunun kimin sorumluluğunda olduğuna karar verir. `https://example.com/product/123` ile bir ürünü `id: 123` eylem olarak isteyen **sunucu** `Product` için bir iş olduğuna karar verir. Sunucu + eylem çiftlerini iki nokta üst üste ile ayırarak `Product:show` şeklinde yazmak iyi bir alışkanlıktır. -Böylece yönlendirici URL'yi bir `Presenter:action` + parametreler çiftine dönüştürdü, bizim durumumuzda `Product:show` + `id: 123`. Bir yönlendiricinin nasıl göründüğünü `app/Router/RouterFactory.php` dosyasında görebilirsiniz ve bunu [Yönlendirme |Routing] bölümünde ayrıntılı olarak açıklayacağız. +Böylece yönlendirici URL'yi bir `Presenter:action` + parametreler çiftine dönüştürdü, bizim durumumuzda `Product:show` + `id: 123`. Bir yönlendiricinin nasıl göründüğünü `app/Core/RouterFactory.php` dosyasında görebilirsiniz ve bunu [Yönlendirme |Routing] bölümünde ayrıntılı olarak açıklayacağız. Devam edelim. Uygulama zaten sunucunun adını biliyor ve devam edebilir. Sunum yapan kişinin kodu olan `ProductPresenter` nesnesini oluşturarak `Product`. Daha doğrusu, DI konteynerinden sunucuyu yaratmasını ister, çünkü nesneleri üretmek onun işidir. @@ -121,12 +121,9 @@ Böylece, kodu kurgusal bir örnek olan `renderShow(123)` yöntemi çağrıldı, Daha sonra, sunum yapan kişi yanıtı döndürür. Bu bir HTML sayfası, bir resim, bir XML belgesi, diskten bir dosya gönderme, JSON veya başka bir sayfaya yönlendirme olabilir. Daha da önemlisi, nasıl yanıt verileceğini açıkça belirtmezsek ( `ProductPresenter` adresinde olduğu gibi), yanıt şablonu bir HTML sayfası ile işlemek olacaktır. Neden mi? Çünkü vakaların %99'unda bir şablon çizmek isteriz, dolayısıyla sunum yapan kişi bu davranışı varsayılan olarak kabul eder ve işimizi kolaylaştırmak ister. Nette'in amacı da bu. -Hangi şablonun çizileceğini belirtmemize bile gerek yok, basit bir mantıkla o şablona giden yolu türetiyor. Sunucu `Product` ve eylem `show` durumunda, bu şablon dosyalarından birinin `ProductPresenter` sınıfının bulunduğu dizine göre var olup olmadığını görmeye çalışır: +Hangi şablonun işleneceğini belirtmemize bile gerek yoktur; framework yolu kendisi çıkaracaktır. `show` eylemi söz konusu olduğunda, basitçe `ProductPresenter` sınıfının bulunduğu dizindeki `show.latte` şablonunu yüklemeye çalışır. Ayrıca `@layout.latte` dosyasındaki düzeni bulmaya çalışır ( [şablon arama |templates#Template Lookup] hakkında daha fazla bilgi). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -Ayrıca `@layout.latte` dosyasında düzeni bulmaya çalışacak ve ardından şablonu oluşturacaktır. Artık sunucunun ve tüm uygulamanın görevi tamamlanmıştır. Şablon mevcut değilse, 404 hatalı bir sayfa döndürülecektir. Sunumcular hakkında daha fazla bilgiyi [Sunumcular |Presenters] sayfasında bulabilirsiniz. +Daha sonra şablonlar işlenir. Bu, sunucunun ve tüm uygulamanın görevini tamamlar ve iş biter. Eğer şablon mevcut değilse, 404 hata sayfası döndürülür. Sunucular hakkında daha fazla bilgi için [Sunucular |presenters] sayfasına bakabilirsiniz. [* request-flow.svg *] @@ -137,7 +134,7 @@ Sadece emin olmak için, tüm süreci biraz farklı bir URL ile özetlemeye çal 3) yönlendirici URL'yi bir çift olarak çözer `Home:default` 4) bir `HomePresenter` nesnesi oluşturulur 5) `renderDefault()` yöntemi çağrılır (eğer varsa) -6) `templates/@layout.latte` düzenine sahip bir `templates/Home/default.latte` şablonu oluşturulur +6) `@layout.latte` düzenine sahip bir `default.latte` şablonu oluşturulur Şu anda birçok yeni kavramla karşılaşmış olabilirsiniz, ancak bunların anlamlı olduğuna inanıyoruz. Nette'de uygulama oluşturmak çocuk oyuncağı. diff --git a/application/tr/modules.texy b/application/tr/modules.texy deleted file mode 100644 index e447d581bd..0000000000 --- a/application/tr/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Modüller -******** - -.[perex] -Nette'de modüller bir uygulamayı oluşturan mantıksal birimleri temsil eder. Sunucuları, şablonları, muhtemelen bileşenleri ve model sınıflarını içerirler. - -Sunucular için bir dizin ve şablonlar için bir dizin gerçek projeler için yeterli olmayacaktır. Bir klasörde düzinelerce dosya olması en azından düzensizdir. Bundan nasıl kurtulabiliriz? Basitçe onları diskteki alt dizinlere ve koddaki ad alanlarına böleriz. Nette modüllerinin yaptığı da tam olarak budur. - -Sunucular ve şablonlar için tek bir klasör oluşturmayı unutalım ve bunun yerine örneğin `Admin` ve `Front` gibi modüller oluşturalım. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← directory with modules -│ ├── Admin/ ← module Admin -│ │ ├── Presenters/ ← its presenters -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← module Front -│ └── Presenters/ ← its presenters -│ └── ... -\-- - -Bu dizin yapısı sınıf ad alanları tarafından yansıtılacaktır, bu nedenle örneğin `DashboardPresenter` `App\Modules\Admin\Presenters` ad alanında olacaktır: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -`Admin` modülü içindeki `Dashboard` sunumcusuna uygulama içinde iki nokta üst üste gösterimi kullanılarak `Admin:Dashboard` şeklinde ve `default` eylemine `Admin:Dashboard:default` şeklinde referans verilir. -Peki Nette proper `Admin:Dashboard` 'un `App\Modules\Admin\Presenters\DashboardPresenter` sınıfını temsil ettiğini nasıl biliyor? Bu, yapılandırmadaki [eşleme |#mapping] ile belirlenir. -Bu nedenle, verilen yapı sabit değildir ve ihtiyaçlarınıza göre değiştirebilirsiniz. - -Modüller elbette sunum yapanların ve şablonların yanı sıra bileşenler, model sınıfları vb. gibi diğer tüm öğeleri de içerebilir. - - -İç İçe Modüller .[#toc-nested-modules] --------------------------------------- - -Modüller sadece düz bir yapı oluşturmak zorunda değildir, örneğin alt modüller de oluşturabilirsiniz: - -/--pre -app/ -├── Modules/ ← directory with modules -│ ├── Blog/ ← module Blog -│ │ ├── Admin/ ← submodule Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← submodule Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← module Forum -│ │ └── ... -\-- - -Böylece, `Blog` modülü `Admin` ve `Front` alt modüllerine bölünmüştür. Yine, bu durum `App\Modules\Blog\Admin\Presenters` vb. isim alanlarına da yansıyacaktır. Alt modülün içindeki `Dashboard` sunucusu `Blog:Admin:Dashboard` olarak adlandırılır. - -İç içe geçme istediğiniz kadar derin olabilir, böylece alt alt modüller oluşturulabilir. - - -Bağlantı Oluşturma .[#toc-creating-links] ------------------------------------------ - -Sunucu şablonlarındaki bağlantılar geçerli modüle görelidir. Bu nedenle, `Foo:default` bağlantısı geçerli sunum yapan kişiyle aynı modülde bulunan `Foo` sunum yapan kişiye yönlendirir. Örneğin, geçerli modül `Front` ise, bağlantı şu şekilde olur: - -```latte -link to Front:Product:show -``` - -Bir bağlantı, bir modülün adını içerse bile görelidir ve bu durumda bir alt modül olarak kabul edilir: - -```latte -link to Front:Shop:Product:show -``` - -Mutlak bağlantılar diskteki mutlak yollara benzer şekilde yazılır, ancak eğik çizgiler yerine iki nokta üst üste konur. Böylece, mutlak bir bağlantı iki nokta üst üste ile başlar: - -```latte -link to Admin:Product:show -``` - -Belirli bir modülde mi yoksa onun alt modülünde mi olduğumuzu öğrenmek için `isModuleCurrent(moduleName)` fonksiyonunu kullanabiliriz. - -```latte -
  • - ... -
  • -``` - - -Yönlendirme .[#toc-routing] ---------------------------- - -[Yönlendirme ile ilgili bölüme |routing#Modules] bakın. - - -Haritalama .[#toc-mapping] --------------------------- - -Sınıf adının sunum yapan kişinin adından türetildiği kuralları tanımlar. Bunları [yapılandırmada |configuration] `application › mapping` anahtarının altına yazıyoruz. - -Modül kullanmayan bir örnekle başlayalım. Sadece sunum yapan sınıfların `App\Presenters` ad alanına sahip olmasını isteyeceğiz. Bu, `Home` gibi bir sunucunun `App\Presenters\HomePresenter` sınıfıyla eşleşmesi gerektiği anlamına gelir. Bu, aşağıdaki yapılandırma ile gerçekleştirilebilir: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -Sunum yapan kişinin adı sınıf maskesindeki yıldız işaretiyle değiştirilir ve sonuç sınıf adı olur. Kolay! - -Sunum yapanları modüllere ayırırsak, her modül için kendi eşlememizi yapabiliriz: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Şimdi `Front:Home` sunucusu `App\Modules\Front\Presenters\HomePresenter` sınıfıyla ve `Admin:Dashboard` sunucusu `App\Modules\Admin\Presenters\DashboardPresenter` sınıfıyla eşleşir. - -İlk ikisini değiştirmek için genel bir (yıldız) kural oluşturmak daha pratiktir. Ekstra yıldız işareti sadece modül için sınıf maskesine eklenecektir: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Peki ya iç içe modüller kullanıyorsak ve bir sunumcumuz varsa `Admin:User:Edit`? Bu durumda, her seviye için modülü temsil eden yıldız işaretli bölüm basitçe tekrarlanır ve sonuç `App\Modules\Admin\User\Presenters\EditPresenter` sınıfı olur. - -Alternatif bir gösterim, bir dize yerine üç segmentten oluşan bir dizi kullanmaktır. Bu gösterim bir öncekine eşdeğerdir: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -Varsayılan değer `*: *Module\*Presenter`'dur. diff --git a/application/tr/presenters.texy b/application/tr/presenters.texy index 35e16c86d9..78a564da6d 100644 --- a/application/tr/presenters.texy +++ b/application/tr/presenters.texy @@ -60,7 +60,7 @@ Yönteme benzer şekilde `render()`. O sırada `render()` 'de daha s Bu önemlidir `action()` daha önce çağrılır `render()`Bu nedenle, içinde muhtemelen yaşam döngüsünün bir sonraki seyrini değiştirebiliriz, yani oluşturulacak şablonu ve ayrıca yöntemi değiştirebiliriz `render()` `setView('otherView')` kullanılarak çağrılacaktır. -İstekten gelen parametreler yönteme aktarılır. Parametreler için tür belirtmek mümkündür ve önerilir, örneğin `actionShow(int $id, string $slug = null)` - `id` parametresi eksikse veya tamsayı değilse, sunum yapan kişi [404 hatası |#Error 404 etc.] döndürür ve işlemi sonlandırır. +İstekten gelen parametreler yönteme aktarılır. Parametreler için tür belirtmek mümkündür ve önerilir, örneğin `actionShow(int $id, ?string $slug = null)` - `id` parametresi eksikse veya tamsayı değilse, sunum yapan kişi [404 hatası |#Error 404 etc.] döndürür ve işlemi sonlandırır. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ $this->redirect(/* ... */); Hata 404 vb. .[#toc-error-404-etc] ================================== -Örneğin görüntülemek istediğimiz makale veritabanında bulunmadığı için isteği yerine getiremediğimizde, HTTP hatası 404'ü temsil eden `error(string $message = null, int $httpCode = 404)` yöntemini kullanarak 404 hatasını atacağız: +Örneğin görüntülemek istediğimiz makale veritabanında bulunmadığı için isteği yerine getiremediğimizde, HTTP hatası 404'ü temsil eden `error(?string $message = null, int $httpCode = 404)` yöntemini kullanarak 404 hatasını atacağız: ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +İstek Parametreleri .[#toc-request-parameters] +============================================== + +Sunucu ve her bileşen, parametrelerini HTTP isteğinden alır. Değerleri `getParameter($name)` yöntemi veya `getParameters()` kullanılarak alınabilir. Değerler dizeler veya dizelerin dizileridir, esasen doğrudan URL'den elde edilen ham verilerdir. + +Daha fazla kolaylık için, parametreleri özellikler aracılığıyla erişilebilir hale getirmenizi öneririz. Bunları basitçe şu şekilde açıklayın `#[Parameter]` nitelik: + +```php +use Nette\Application\Attributes\Parameter; // bu hat önemli + +class HomePresenter extends Nette\Application\UI\Presenter +{ + #[Parameter] + public string $theme; // halka açık olmalı +} +``` + +Özellikler için veri türünü belirtmenizi öneririz (örneğin, `string`). Nette daha sonra değeri buna göre otomatik olarak dökecektir. Parametre değerleri de [doğrulanabilir |#Validation of Parameters]. + +Bir bağlantı oluştururken, parametreler için değeri doğrudan ayarlayabilirsiniz: + +```latte +click +``` + + Kalıcı Parametreler .[#toc-persistent-parameters] ================================================= @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter `$this->lang`, `'en'` gibi bir değere sahipse, `link()` veya `n:href` kullanılarak oluşturulan bağlantılar da `lang=en` parametresini içerecektir. Ve bağlantı tıklandığında, yine `$this->lang = 'en'` olacaktır. -Özellikler için veri türünü eklemenizi öneririz (örn. `string`) ve ayrıca varsayılan bir değer de ekleyebilirsiniz. Parametre değerleri [doğrulanabilir |#Validation of Persistent Parameters]. +Özellikler için veri türünü eklemenizi öneririz (örn. `string`) ve ayrıca varsayılan bir değer de ekleyebilirsiniz. Parametre değerleri [doğrulanabilir |#Validation of Parameters]. Kalıcı parametreler varsayılan olarak belirli bir sunum yapan kişinin tüm eylemleri arasında aktarılır. Bunları birden fazla sunum yapan kişi arasında geçirmek için tanımlamanız gerekir: @@ -307,18 +333,12 @@ Daha Derine Gitmek .[#toc-going-deeper] Bu bölümde şimdiye kadar gösterdiklerimiz muhtemelen yeterli olacaktır. Aşağıdaki satırlar, sunum yapanlarla derinlemesine ilgilenen ve her şeyi bilmek isteyenlere yöneliktir. -Gereksinim ve Parametreler .[#toc-requirement-and-parameters] -------------------------------------------------------------- - -Sunum yapan kişi tarafından ele alınan istek [api:Nette\Application\Request] nesnesidir ve sunum yapan kişinin `getRequest()` yöntemi tarafından döndürülür. Bir dizi parametre içerir ve bunların her biri ya bazı bileşenlere ya da doğrudan sunum yapan kişiye aittir (aslında bu da özel bir bileşen olsa da bir bileşendir). Dolayısıyla Nette parametreleri yeniden dağıtır ve `loadState(array $params)` yöntemini çağırarak tek tek bileşenler (ve sunum yapan kişi) arasında geçiş yapar. Parametreler `getParameters(): array` yöntemiyle, ayrı ayrı `getParameter($name)` kullanılarak elde edilebilir. Parametre değerleri dizeler veya dizelerin dizileridir, temelde doğrudan bir URL'den elde edilen ham verilerdir. +Parametrelerin Doğrulanması .[#toc-validation-of-parameters] +------------------------------------------------------------ +URL'lerden alınan [istek parametr |#request parameters] eleri ve [kalıcı parametrelerin |#persistent parameters] değerleri `loadState()` metodu tarafından özelliklere yazılır. Ayrıca özellikte belirtilen veri tipinin eşleşip eşleşmediğini kontrol eder, aksi takdirde 404 hatası ile yanıt verir ve sayfa görüntülenmez. -Kalıcı Parametrelerin Doğrulanması .[#toc-validation-of-persistent-parameters] ------------------------------------------------------------------------------- - -URL'lerden alınan [kalıcı parametrelerin |#persistent parameters] değerleri `loadState()` metodu tarafından özelliklere yazılır. Ayrıca özellikte belirtilen veri türünün eşleşip eşleşmediğini kontrol eder, aksi takdirde 404 hatası ile yanıt verir ve sayfa görüntülenmez. - -Kalıcı parametrelere asla körü körüne güvenmeyin, çünkü bunlar URL'de kullanıcı tarafından kolayca üzerine yazılabilir. Örneğin, `$this->lang` adresinin desteklenen diller arasında olup olmadığını bu şekilde kontrol ediyoruz. Bunu yapmanın iyi bir yolu, yukarıda bahsedilen `loadState()` yöntemini geçersiz kılmaktır: +URL'de kullanıcı tarafından kolayca üzerine yazılabilecekleri için parametrelere asla körü körüne güvenmeyin. Örneğin, `$this->lang` adresinin desteklenen diller arasında olup olmadığını bu şekilde kontrol ederiz. Bunu yapmanın iyi bir yolu, yukarıda bahsedilen `loadState()` yöntemini geçersiz kılmaktır: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,7 +361,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Talebi Kaydetme ve Geri Yükleme .[#toc-save-and-restore-the-request] -------------------------------------------------------------------- -Geçerli isteği bir oturuma kaydedebilir veya oturumdan geri yükleyebilir ve sunucunun tekrar yürütmesine izin verebilirsiniz. Bu, örneğin bir kullanıcı bir formu doldurduğunda ve oturum açma süresi dolduğunda kullanışlıdır. Veri kaybetmemek için, oturum açma sayfasına yönlendirmeden önce, kısa bir dize biçiminde bir tanımlayıcı döndüren ve oturum açma sunucusuna parametre olarak ileten `$reqId = $this->storeRequest()` adresini kullanarak geçerli isteği oturuma kaydederiz. +Sunum yapan kişinin ele aldığı istek bir [api:Nette\Application\Request] nesnesidir ve sunum yapan kişinin `getRequest()` yöntemi tarafından döndürülür. + +Geçerli isteği bir oturuma kaydedebilir veya oturumdan geri yükleyebilir ve sunum yapan kişinin bunu tekrar yürütmesine izin verebilirsiniz. Bu, örneğin bir kullanıcı bir formu doldurduğunda ve oturum açma süresi dolduğunda kullanışlıdır. Verileri kaybetmemek için, oturum açma sayfasına yönlendirmeden önce, kısa bir dize biçiminde bir tanımlayıcı döndüren ve oturum açma sunucusuna parametre olarak ileten `$reqId = $this->storeRequest()` adresini kullanarak geçerli isteği oturuma kaydederiz. Oturum açtıktan sonra, isteği oturumdan alan ve ona ileten `$this->restoreRequest($reqId)` yöntemini çağırıyoruz. Yöntem, isteğin şu anda oturum açmış olan kullanıcıyla aynı kullanıcı tarafından oluşturulduğunu doğrular. Başka bir kullanıcı giriş yaparsa veya anahtar geçersizse, hiçbir şey yapmaz ve program devam eder. @@ -362,7 +384,7 @@ Yeniden yönlendirme bir AJAX veya POST isteği ile gerçekleşmez çünkü veri Ayrıca, `link()` yöntemi gibi sunum yapan kişiyi, eylemleri ve parametreleri bağımsız değişken olarak alan `canonicalize()` yöntemini kullanarak kanonlaştırmayı manuel olarak da çağırabilirsiniz. Bir bağlantı oluşturur ve bunu geçerli URL ile karşılaştırır. Farklıysa, oluşturulan bağlantıya yönlendirir. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // eğer $slug, $realSlug'dan farklıysa yönlendirir @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Erişim Kısıtlaması Kullanımı `#[Requires]` .[#toc-access-restriction-using-requires]{data-version:3.2.2} +-------------------------------------------------------------------------------------------------------- + +Bu `#[Requires]` özniteliği, sunum yapanlara ve yöntemlerine erişimi kısıtlamak için gelişmiş seçenekler sağlar. HTTP yöntemlerini belirtmek, AJAX istekleri gerektirmek, erişimi aynı kaynakla sınırlamak ve erişimi yalnızca yönlendirme ile kısıtlamak için kullanılabilir. Öznitelik, sunum yapan sınıfların yanı sıra aşağıdaki gibi bireysel yöntemlere de uygulanabilir `action()`, `render()`, `handle()`ve `createComponent()`. + +Bu kısıtlamaları belirtebilirsiniz: +- HTTP yöntemleri üzerinde: `#[Requires(methods: ['GET', 'POST'])]` +- AJAX isteği gerektiriyor: `#[Requires(ajax: true)]` +- yalnızca aynı kaynaktan erişim: `#[Requires(sameOrigin: true)]` +- yalnızca yönlendirme yoluyla erişim: `#[Requires(forward: true)]` +- belirli eylemlere ilişkin kısıtlamalar: `#[Requires(actions: 'default')]` + +Ayrıntılar için, bkz [Nasıl kullanılır Requires öznitelik |best-practices:attribute-requires]. + + +HTTP Yöntem Kontrolü .[#toc-http-method-check] +---------------------------------------------- + +Nette'de sunum yapanlar, öncelikle güvenlik nedenleriyle gelen her isteğin HTTP yöntemini otomatik olarak doğrular. Varsayılan olarak `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` yöntemlerine izin verilir. + + `OPTIONS` gibi ek yöntemleri etkinleştirmek istiyorsanız `#[Requires]` özniteliği (Nette Uygulaması v3.2'den): + +```php +#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] +class MyPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Sürüm 3.1'de doğrulama, istekte belirtilen yöntemin `$presenter->allowedMethods` dizisine dahil edilip edilmediğini kontrol eden `checkHttpMethod()` içinde gerçekleştirilir. Bunun gibi bir yöntem ekleyin: + +```php +class MyPresenter extends Nette\Application\UI\Presenter +{ + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } +} +``` + + `OPTIONS` yöntemini etkinleştirirseniz, bunu sunucunuzda da düzgün bir şekilde ele almanız gerektiğini vurgulamak çok önemlidir. Bu yöntem genellikle, tarayıcıların CORS (Cross-Origin Resource Sharing) politikası açısından isteğe izin verilip verilmediğini belirlemek için gerekli olduğunda asıl istekten önce otomatik olarak gönderdiği bir ön kontrol isteği olarak kullanılır. Bu yönteme izin verir ancak uygun bir yanıt uygulamazsanız, tutarsızlıklara ve potansiyel güvenlik sorunlarına yol açabilir. + + Daha Fazla Okuma .[#toc-further-reading] ======================================== diff --git a/application/tr/routing.texy b/application/tr/routing.texy index cbd1656234..fd138735da 100644 --- a/application/tr/routing.texy +++ b/application/tr/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Ya da bu formu kullanabiliriz, doğrulama düzenli ifadesinin yeniden yazıldığına dikkat edin: +Daha ayrıntılı bir belirtim için, varsayılan değerlere ek olarak, doğrulama düzenli ifadesi gibi diğer parametre özelliklerinin de ayarlanabildiği daha da genişletilmiş bir form kullanılabilir (bkz. `id` parametresi): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Bu daha konuşkan formatlar diğer meta verileri eklemek için kullanışlıdır. +Dizide tanımlanan parametreler yol maskesine dahil edilmemişse, URL'de bir soru işaretinden sonra belirtilen sorgu parametreleri kullanılsa bile değerlerinin değiştirilemeyeceğine dikkat etmek önemlidir. Filtreler ve Çeviriler .[#toc-filters-and-translations] @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Modüller .[#toc-modules] ------------------------ -Bir [modüle |modules] ait daha fazla rotamız varsa, bunları gruplamak için `withModule()` adresini kullanabiliriz: +Bir [modüle |directory-structure#Presenters and Templates] ait daha fazla rotamız varsa, bunları gruplamak için `withModule()` adresini kullanabiliriz: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Entegrasyon .[#toc-integration] =============================== -Yönlendiricimizi uygulamaya bağlamak için DI konteynerine bunu söylemeliyiz. Bunun en kolay yolu router nesnesini oluşturacak fabrikayı hazırlamak ve container konfigürasyonuna bunu kullanmasını söylemektir. Diyelim ki bu amaçla bir metot yazdık `App\Router\RouterFactory::createRouter()`: +Yönlendiricimizi uygulamaya bağlamak için DI konteynerine bunu söylemeliyiz. Bunun en kolay yolu router nesnesini oluşturacak fabrikayı hazırlamak ve container konfigürasyonuna bunu kullanmasını söylemektir. Diyelim ki bu amaçla bir metot yazdık `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ Daha sonra [yapılandırmaya |dependency-injection:services] yazıyoruz: ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Veritabanı bağlantısı vb. gibi tüm bağımlılıklar, [otomatik |dependency-injection:autowiring] bağlantı kullanılarak fabrika yöntemine parametreleri olarak aktarılır: @@ -663,7 +663,7 @@ Ayrı kullanımla, yönlendiricinin yeteneklerinin Nette Uygulaması ve sunucula Bu yüzden yine örneğin bir yönlendirici oluşturacak bir yöntem oluşturacağız: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Ya da doğrudan nesneler oluşturacağız: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/tr/templates.texy b/application/tr/templates.texy index d817bc2d45..639c969839 100644 --- a/application/tr/templates.texy +++ b/application/tr/templates.texy @@ -34,35 +34,81 @@ Bu da eylem şablonu olabilir: Düzende `{include content}` yerine eklenen `content` bloğunu tanımlar ve ayrıca düzende `{block title}` 'un üzerine yazılan `title` bloğunu yeniden tanımlar. Sonucu hayal etmeye çalışın. -Şablon Arama .[#toc-search-for-templates] ------------------------------------------ +Şablon Arama .[#toc-template-lookup] +------------------------------------ + +Sunucularda, hangi şablonun işleneceğini belirtmenize gerek yoktur; çerçeve yolu otomatik olarak belirleyerek kodlamayı sizin için kolaylaştıracaktır. + +Her sunucunun kendi dizinine sahip olduğu bir dizin yapısı kullanıyorsanız, şablonu bu dizine eylemin adı (yani görünüm) altında yerleştirmeniz yeterlidir. Örneğin, `default` eylemi için `default.latte` şablonunu kullanın: -Şablonların yolu basit bir mantığa göre çıkarılır. Sunucu sınıfının bulunduğu dizine göre bu şablon dosyalarından birinin var olup olmadığına bakılır, burada `` geçerli sunum yapan kişinin adı ve `` geçerli eylemin adıdır: +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -- `templates//.latte` -- `templates/..latte` +Sunucuların tek bir dizinde ve şablonların `templates` klasöründe bir arada olduğu bir yapı kullanıyorsanız, bunu bir dosyaya kaydedin `..latte` veya `/.latte`: -Şablon bulunamazsa, `templates` dizininde bir seviye yukarıda, yani sunum yapan sınıfın bulunduğu dizinle aynı seviyede arama yapmaya çalışacaktır. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- -Şablon orada da bulunamazsa, yanıt [404 hatası |presenters#Error 404 etc.] olur. + `templates` dizini, sunum yapan sınıfların bulunduğu dizinle aynı seviyede, bir seviye daha yükseğe de yerleştirilebilir. -Ayrıca `$this->setView('otherView')` adresini kullanarak görünümü değiştirebilirsiniz. Ya da arama yapmak yerine `$this->template->setFile('/path/to/template.latte')` adresini kullanarak şablon dosyasının adını doğrudan belirtin. +Şablon bulunamazsa, sunum yapan kişi [404 - sayfa bulunamadı hatası |presenters#Error 404 etc] ile yanıt verir. + +Görünümü `$this->setView('anotherView')` adresini kullanarak değiştirebilirsiniz. Şablon dosyasını `$this->template->setFile('/path/to/template.latte')` ile doğrudan belirtmek de mümkündür. .[note] -Olası dosya yollarından oluşan bir dizi döndüren [formatTemplateFiles |api:Nette\Application\UI\Presenter::formatTemplateFiles()] yöntemini geçersiz kılarak şablonların arandığı yolları değiştirebilirsiniz. +Şablonların arandığı dosyalar, olası dosya adlarından oluşan bir dizi döndüren [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()] yöntemi geçersiz kılınarak değiştirilebilir. + + +Düzen Şablonu Arama .[#toc-layout-template-lookup] +-------------------------------------------------- + +Nette ayrıca otomatik olarak yerleşim dosyasını arar. + +Her sunum yapan kişinin kendi dizinine sahip olduğu bir dizin yapısı kullanıyorsanız, düzeni ya sadece sunum yapan kişiye özelse sunum yapan kişinin bulunduğu klasöre ya da birden fazla sunum yapan kişi için ortaksa bir üst seviyeye yerleştirin: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +Sunum yapanların tek bir dizinde gruplandığı ve şablonların `templates` klasöründe bulunduğu bir yapı kullanıyorsanız, düzen aşağıdaki yerlerde beklenecektir: -Düzen aşağıdaki dosyalarda beklenmektedir: +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` birden fazla sunumcu için ortak düzen +Sunucu bir modüldeyse, modülün iç içe geçmesine göre dizin ağacının daha yukarısında da arama yapacaktır. -`` geçerli sunum yapan kişinin adı ve `` varsayılan olarak `'layout'` olan düzenin adıdır. İsim `$this->setLayout('otherLayout')` ile değiştirilebilir, böylece `@otherLayout.latte` dosyaları denenecektir. +Düzenin adı `$this->setLayout('layoutAdmin')` kullanılarak değiştirilebilir ve ardından `@layoutAdmin.latte` dosyasında beklenir. Düzen şablon dosyasını `$this->setLayout('/path/to/template.latte')` adresini kullanarak doğrudan da belirtebilirsiniz. -Düzen şablonunun dosya adını `$this->setLayout('/path/to/template.latte')` adresini kullanarak doğrudan da belirtebilirsiniz. `$this->setLayout(false)` adresini kullanmak düzen aramayı devre dışı bırakacaktır. +Şablon içinde `$this->setLayout(false)` veya `{layout none}` etiketinin kullanılması düzen aramayı devre dışı bırakır. .[note] -Olası dosya yollarından oluşan bir dizi döndüren [formatLayoutTemplateFiles |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] yöntemini geçersiz kılarak şablonların arandığı yolları değiştirebilirsiniz. +Düzen şablonlarının arandığı dosyalar, olası dosya adlarından oluşan bir dizi döndüren [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] yöntemi geçersiz kılınarak değiştirilebilir. Şablondaki Değişkenler .[#toc-variables-in-the-template] @@ -104,7 +150,7 @@ Sunucudaki `$this->template` nesnesi artık `ArticleTemplate` sınıfının bir Şablonlarda da fısıldama lüksünün tadını çıkarabilirsiniz, sadece PhpStorm'da Latte eklentisini kurun ve şablonun başında sınıf adını belirtin, "Latte: sistem nasıl yazılır":https://blog.nette.org/tr/latte-tip-sistemi-nasil-kullanilir makalesine bakın: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void Latte sürüm 3, her web projesi için bir [uzantı |latte:creating-extension] oluşturarak daha gelişmiş bir yol sunar. İşte böyle bir sınıfın kabaca bir örneği: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ final class LatteExtension extends Latte\Extension ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ Alternatif olarak, çevirmen [yapılandırma |configuration#Latte] kullanılarak ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` Çevirmen daha sonra örneğin `|translate` filtresi olarak kullanılabilir ve ek parametreler `translate()` yöntemine aktarılabilir (bkz. `foo, bar`): diff --git a/application/uk/@home.texy b/application/uk/@home.texy index c1bd9272d7..5dc9bb0fab 100644 --- a/application/uk/@home.texy +++ b/application/uk/@home.texy @@ -1,36 +1,85 @@ -Nette Application -***************** +Додаток Nette +************* .[perex] -Пакет `nette/application` є основою для створення інтерактивних веб-додатків. +Nette Application - це ядро фреймворку Nette, що надає потужні інструменти для створення сучасних веб-додатків. Він пропонує численні виняткові можливості, які значно спрощують розробку та покращують безпеку коду і зручність його супроводу. -- [Як працюють додатки? |how-it-works] -- [Bootstrap |Bootstrap] -- [Презентери |presenters] -- [Шаблони |templates] -- [Модулі |modules] -- [Маршрутизація |routing] -- [Створення URL-посилань |creating-links] -- [Інтерактивні компоненти |components] -- [AJAX і сніпети |ajax] -- [Multiplier |Multiplier] -- [Конфігурація |configuration] +Встановлення .[#toc-installation] +--------------------------------- -Встановлення ------------- - -Завантажте та встановіть пакет за допомогою [Composer |best-practices:composer]: +Завантажте та встановіть бібліотеку за допомогою [Composer |best-practices:composer]: ```shell composer require nette/application ``` -| версія пакета | сумісна версія PHP -|-----------------------|----------------------- -| Nette Application 4.0 | PHP 8.0 - 8.1 -| Nette Application 3.1 | PHP 7.2 - 8.1 + +Чому варто обрати Nette Application? .[#toc-why-choose-nette-application] +------------------------------------------------------------------------- + +Компанія Nette завжди була піонером у галузі веб-технологій. + +**Двонаправлений маршрутизатор:** Nette має вдосконалену систему маршрутизації, унікальну своєю двонаправленістю - вона не тільки перекладає URL-адреси на дії додатків, але й може генерувати URL-адреси у зворотному напрямку. Це означає: +- Ви можете будь-коли змінити структуру URL-адреси всього додатка, не змінюючи файли шаблонів +- URL-адреси автоматично канонізуються, що покращує SEO +- Маршрутизація визначається в одному місці, а не розкидана в анотаціях + +**Компоненти та сигнали:** Вбудована система компонентів, натхненна Delphi та React.js, є унікальною серед PHP-фреймворків: +- Дозволяє створювати багаторазові елементи інтерфейсу користувача +- Підтримує ієрархічну композицію компонентів +- Пропонує елегантну обробку 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 з системою типів + +**Ін'єкція залежностей:** Nette повністю використовує ін'єкцію залежностей: +- Автоматична передача залежностей (автопідключення) +- Конфігурація з використанням зрозумілого формату NEON +- Підтримка фабрик компонентів + + +Основні переваги .[#toc-main-benefits] +-------------------------------------- + +- **Безпека**: Автоматичний захист від таких [вразливостей |nette:vulnerability-protection], як XSS, CSRF тощо. +- Продуктивність: Менше коду, більше функцій завдяки розумному дизайну +- Налагодження**: [Відладчик Tracy |tracy:] з панеллю маршрутизації +- Продуктивність**: Інтелектуальна система кешування, ліниве завантаження компонентів +- Гнучкість**: Легка модифікація URL-адреси навіть після завершення роботи програми +- **Компоненти**: Унікальна система багаторазового використання елементів інтерфейсу +- **Сучасність**: Повна підтримка PHP 8.4+ та системи типів + + +Початок роботи .[#toc-getting-started] +-------------------------------------- + +1. [Розуміння додатків |how-it-works] - розуміння базової архітектури +2. [Презентатори |presenters] - Робота з презентаторами та діями +3. [Шаблони |templates] - Створення шаблонів у Latte +4. [Маршрутизація |routing] - Налаштування URL-адрес +5. [Інтерактивні компоненти |components] - Використання системи компонентів + + +Сумісність з PHP .[#toc-php-compatibility] +------------------------------------------ + +| версія | сумісна з 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 -Застосовується до останніх версій патчів. +Дійсно для останніх версій патчів. \ No newline at end of file diff --git a/application/uk/@left-menu.texy b/application/uk/@left-menu.texy index 1f6999ed7d..5a6862d2d5 100644 --- a/application/uk/@left-menu.texy +++ b/application/uk/@left-menu.texy @@ -4,7 +4,7 @@ - [Bootstrap |Bootstrap] - [Презентери |presenters] - [Шаблони |templates] -- [Модулі |modules] +- [Структура каталогу |directory-structure] - [Маршрутизація |routing] - [Створення URL-посилань |creating-links] - [Інтерактивні компоненти |components] diff --git a/application/uk/ajax.texy b/application/uk/ajax.texy index 657629160c..9a6e49c71c 100644 --- a/application/uk/ajax.texy +++ b/application/uk/ajax.texy @@ -1,12 +1,12 @@ -AJAX і сніпети -************** +AJAX та сніппети +****************
    -Сучасні веб-додатки сьогодні працюють наполовину на сервері, а наполовину в браузері. AJAX є життєво важливим об'єднуючим фактором. Яку підтримку пропонує фреймворк Nette? -- надсилання фрагментів шаблонів (так званих *сніпетів*) -- передача змінних між PHP і JavaScript -- Налагодження додатків AJAX +В епоху сучасних веб-додатків, де функціональність часто поширюється між сервером і браузером, AJAX є важливим сполучним елементом. Які можливості пропонує Nette Framework у цій сфері? +- надсилання частин шаблону, так званих фрагментів (snippets) +- передача змінних між PHP та JavaScript +- інструменти для налагодження AJAX-запитів
    @@ -14,29 +14,32 @@ AJAX і сніпети AJAX запит .[#toc-ajax-request] =============================== -AJAX-запит нічим не відрізняється від класичного запиту - викликається доповідач з певним представленням і параметрами. Як на нього відповісти, також залежить від доповідача: він може використовувати власну процедуру, яка повертає фрагмент HTML-коду (HTML-сніппет), XML-документ, JSON-об'єкт або JavaScript-код. +AJAX-запит принципово не відрізняється від класичного HTTP-запиту. Викликається презентер з певними параметрами. Ведучий сам вирішує, як відповісти на запит - він може повернути дані у форматі JSON, надіслати частину HTML-коду, XML-документ тощо. -На стороні сервера AJAX-запит можна виявити за допомогою сервісного методу, що [інкапсулює HTTP-запит |http:request] `$httpRequest->isAjax()` (виявляє на основі HTTP-заголовка `X-Requested-With`). Усередині доповідача доступний ярлик у вигляді методу `$this->isAjax()`. +На стороні браузера ми ініціюємо AJAX-запит за допомогою функції `fetch()`: -Існує попередньо оброблений об'єкт `payload`, призначений для надсилання даних у браузер у форматі JSON. - -```php -public function actionDelete(int $id): void -{ - if ($this->isAjax()) { - $this->payload->message = 'Успішно'; - } - // ... -} +```js +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) +.then(response => response.json()) +.then(payload => { + // обробка відповіді +}); ``` -Для повного контролю над виведенням JSON використовуйте метод `sendJson` у презентері. Це негайно перерве роботу презентера, і ви обійдетеся без шаблону: +На стороні сервера AJAX-запит розпізнається методом `$httpRequest->isAjax()` сервісу, що [інкапсулює HTTP-запит |http:request]. Він використовує HTTP-заголовок `X-Requested-With`, тому його обов'язково потрібно відправити. У презентері ви можете використовувати метод `$this->isAjax()`. + +Якщо ви хочете відправити дані у форматі JSON, використовуйте метод [`sendJson()` |presenters#Sending a response] метод. Метод також завершує роботу доповідача. ```php -$this->sendJson(['key' => 'value', /* ... */]); +public function actionExport(): void +{ + $this->sendJson($this->model->getData); +} ``` -Якщо ми хочемо надіслати HTML, ми можемо встановити спеціальний шаблон для AJAX-запитів: +Якщо ви плануєте відповідати за допомогою спеціального шаблону, розробленого для AJAX, ви можете зробити це наступним чином: ```php public function handleClick($param): void @@ -44,27 +47,43 @@ public function handleClick($param): void if ($this->isAjax()) { $this->template->setFile('path/to/ajax.latte'); } - // ... + //... } ``` -Naja .[#toc-naja] -================= +Фрагменти .[#toc-snippets] +========================== + +Найпотужнішим інструментом, який пропонує Nette для зв'язку сервера з клієнтом, є сніппети. З їх допомогою ви можете перетворити звичайний додаток на AJAX з мінімальними зусиллями та кількома рядками коду. Приклад Fifteen демонструє, як це все працює, а його код можна знайти на [GitHub |https://github.com/nette-examples/fifteen]. + +Сніппети, або вирізки, дозволяють оновлювати лише частини сторінки, замість того, щоб перезавантажувати всю сторінку. Це швидше і ефективніше, а також забезпечує більш комфортний користувацький досвід. Сніппети можуть нагадати вам Hotwire для Ruby on Rails або Symfony UX Turbo. Цікаво, що Nette представила сніппети на 14 років раніше. -[Бібліотека Naja |https://naja.js.org] використовується для обробки AJAX-запитів на стороні браузера. [Встановіть |https://naja.js.org/#/guide/01-install-setup-naja] його як пакет node.js (для використання з Webpack, Rollup, Vite, Parcel та іншими): +Як працюють фрагменти? При першому завантаженні сторінки (не AJAX-запит) завантажується вся сторінка, включаючи всі фрагменти. Коли користувач взаємодіє зі сторінкою (наприклад, натискає кнопку, відправляє форму тощо), замість завантаження всієї сторінки виконується AJAX-запит. Код у презентері виконує дію і вирішує, які фрагменти потрібно оновити. Nette рендерить ці фрагменти і надсилає їх у вигляді масиву JSON. Потім код обробки в браузері вставляє отримані фрагменти назад на сторінку. Таким чином, передається лише код змінених фрагментів, що економить пропускну здатність і прискорює завантаження порівняно з передачею всього вмісту сторінки. + + +Наджа .[#toc-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 ``` -...або вставити безпосередньо в шаблон сторінки: +... або вставте її безпосередньо в шаблон сторінки: ```html ``` -Щоб створити AJAX-запит зі звичайного посилання (сигналу) або форми, просто позначте відповідне посилання, форму або кнопку класом `ajax`: +Спочатку потрібно [ініціалізувати |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] бібліотеку: + +```js +naja.initialize(); +``` + +Щоб зробити звичайне посилання (сигнал) або відправку форми AJAX-запитом, просто позначте відповідне посилання, форму або кнопку класом `ajax`: ```html Go @@ -74,64 +93,39 @@ npm install naja or +
    ``` -Сніпети .[#toc-snippety] -======================== - -Однак існує набагато потужніший інструмент вбудованої підтримки AJAX - сніпети. Їхнє використання дає змогу перетворити звичайний застосунок на AJAX-додаток за допомогою лише кількох рядків коду. Як усе це працює, показано в прикладі Fifteen, код якого також доступний у збірці або на [GitHub |https://github.com/nette-examples/fifteen]. - -Принцип роботи сніпетів полягає в тому, що всю сторінку передають під час початкового (тобто не-AJAX) запиту, а потім із кожним AJAX [subrequest |components#Signal] (запит до того самого подання того самого презентера) тільки код змінених частин передають до сховища `payload`, згаданого раніше. - -Сніпети можуть нагадати вам Hotwire для Ruby on Rails або Symfony UX Turbo, але Nette придумав їх чотирнадцятьма роками раніше. - - -Інвалідація .[#toc-invalidation-of-snippets] -============================================ +Перемальовуємо фрагменти .[#toc-redrawing-snippets] +--------------------------------------------------- -Кожен нащадок класу [Control |components] (яким є і Presenter) здатний пам'ятати, чи були якісь зміни під час запиту, що вимагають повторного відображення. Існує кілька способів впоратися з цим: `redrawControl()` і `isControlInvalid()`. Приклад: +Кожен об'єкт класу [Control |components] (в тому числі і сам доповідач) зберігає інформацію про те, чи відбулися зміни, які вимагають його перемальовування. Для цього використовується метод `redrawControl()`. ```php public function handleLogin(string $user): void { - // Об'єкт має повторно відображатися після того, як користувач увійшов у систему + // після входу в систему необхідно перемалювати відповідну частину $this->redrawControl(); - // ... + //... } ``` -Однак Nette забезпечує ще більш тонкий дозвіл, ніж цілі компоненти. Перераховані методи приймають ім'я так званого "фрагмента" як необов'язковий параметр. "Фрагмет" це, по суті, елемент у вашому шаблоні, позначений для цієї мети макросом Latte, докладніше про це пізніше. Таким чином, можна попросити компонент перемалювати тільки *частину* свого шаблону. Якщо весь компонент недійсний, то всі його фрагменти відображаються заново. Компонент є "недійсним", якщо будь-який з його субкомпонентів є недійсним. -```php -$this->isControlInvalid(); // -> false - -$this->redrawControl('header'); // анулює фрагмент з ім'ям 'header' -$this->isControlInvalid('header'); // -> true -$this->isControlInvalid('footer'); // -> false -$this->isControlInvalid(); // -> true, принаймні один фрагмент недійсний +Nette також дозволяє краще контролювати те, що потрібно перемалювати. Вищезгаданий метод може приймати назву фрагмента як аргумент. Таким чином, його можна анулювати (тобто примусово перемалювати) на рівні частини шаблону. Якщо весь компонент анулюється, кожен його фрагмент також перемальовується: -$this->redrawControl(); // робить недійсним весь компонент, кожен фрагмент -$this->isControlInvalid('footer'); // -> true +```php +// робить недійсним фрагмент 'header' +$this->redrawControl('header'); ``` -Компонент, який отримав сигнал, автоматично позначається для перемальовування. - -Завдяки перемальовуванню фрагментів ми точно знаємо, які частини яких елементів мають бути перемальовані. - - -Тег `{snippet} … {/snippet}` .{toc: Tag snippet} -================================================ - -Рендеринг сторінки відбувається так само, як і під час звичайного запиту: завантажуються одні й ті самі шаблони тощо. Однак найважливіше - це не допустити потрапляння до вихідного сигналу тих частин, які не повинні потрапити до вихідного сигналу; інші частини мають бути пов'язані з ідентифікатором і надіслані користувачеві у форматі, зрозумілому для обробника JavaScript. - -Синтаксис .[#toc-sintaksis] ---------------------------- +Фрагменти в Latte .[#toc-snippets-in-latte] +------------------------------------------- -Якщо в шаблоні є елемент управління або фрагмент, ми повинні обернути його за допомогою парного тега `{snippet} ... {/snippet}` - відмальований фрагмент буде "вирізаний" і відправиться в браузер. Він також укладе його в допоміжний тег `
    ` (можна використовувати інший). У наступному прикладі визначено сніппет з ім'ям `header`. Він також може являти собою шаблон компонента: +Використовувати фрагменти в Latte надзвичайно просто. Щоб визначити частину шаблону як фрагмент, просто оберніть його тегами `{snippet}` та `{/snippet}`: ```latte {snippet header} @@ -139,7 +133,9 @@ $this->isControlInvalid('footer'); // -> true {/snippet} ``` -Якщо ви хочете створити сніппет з іншим елементом, що містить, відмінним від `
    `, або додати користувацькі атрибути до елемента, ви можете використовувати таке визначення: +Фрагмент створює елемент `
    ` в HTML-сторінці зі спеціально згенерованим `id`. При перемальовуванні фрагмента вміст цього елемента оновлюється. Тому під час першого відображення сторінки всі сніппети також повинні бути відображені, навіть якщо спочатку вони можуть бути порожніми. + +Ви також можете створити фрагмент з елементом, відмінним від `
    ` використовуючи атрибут n:: ```latte
    @@ -148,138 +144,106 @@ $this->isControlInvalid('footer'); // -> true ``` -Динамічні сніпети .[#toc-dinamiceskie-snippety] -=============================================== +Області фрагментів .[#toc-snippet-areas] +---------------------------------------- -У Nette ви також можете визначити сніпети з динамічним ім'ям, заснованим на параметрі часу виконання. Це найбільше підходить для різних списків, де нам потрібно змінити лише один рядок, але ми не хочемо переносити весь список разом із ним. Прикладом цього може бути: +Назви фрагментів також можуть бути виразами: ```latte - +{foreach $items as $id => $item} +
  • {$item}
  • +{/foreach} ``` -Існує один статичний сніппет `itemsContainer`, що містить кілька динамічних сніпетів: `пункт-0`, `пункт-1` і так далі. +Таким чином, ми отримаємо кілька фрагментів на кшталт `item-0`, `item-1` і т.д. Якщо ми безпосередньо зробимо недійсним динамічний фрагмент (наприклад, `item-1`), нічого не буде перемальовано. Причина в тому, що сніппети функціонують як справжні уривки, і тільки вони безпосередньо рендерингуються. Однак у шаблоні технічно не існує фрагмента з назвою `item-1`. Він з'являється лише при виконанні коду, що оточує фрагмент, в даному випадку - циклу foreach. Тому ми позначимо частину шаблону, яку потрібно виконати, тегом `{snippetArea}`: -Ви не можете перемалювати динамічний фрагмент безпосередньо (перемальовування `item-1` не має ефекту), ви маєте перемалювати його батьківський фрагмент (у цьому прикладі `itemsContainer`). При цьому виконується код батьківського сніпета, але браузеру передаються тільки його вкладені сніпети. Якщо ви хочете передати тільки один із вкладених сніпетів, вам потрібно змінити введення для батьківського сніпета, щоб не генерувати інші вкладені сніпети. +```latte +
      + {foreach $items as $id => $item} +
    • {$item}
    • + {/foreach} +
    +``` -У наведеному прикладі необхідно переконатися, що під час AJAX-запиту до масиву `$list` буде додано тільки один елемент, тому цикл `foreach` виводитиме тільки один динамічний фрагмент. +І перемалюємо як окремий фрагмент, так і всю область, що охоплює його: ```php -class HomePresenter extends Nette\Application\UI\Presenter -{ - /** - * Этот метод возвращает данные для списка. - * Обычно это просто запрос данных из модели. - * Для целей этого примера данные жёстко закодированы. - */ - private function getTheWholeList(): array - { - return [ - 'First', - 'Second', - 'Third', - ]; - } - - public function renderDefault(): void - { - if (!isset($this->template->list)) { - $this->template->list = $this->getTheWholeList(); - } - } - - public function handleUpdate(int $id): void - { - $this->template->list = $this->isAjax() - ? [] - : $this->getTheWholeList(); - $this->template->list[$id] = 'Updated item'; - $this->redrawControl('itemsContainer'); - } -} +$this->redrawControl('itemsContainer'); +$this->redrawControl('item-1'); ``` +Важливо також переконатися, що масив `$items` містить лише ті елементи, які потрібно перемалювати. -Сніпети в увімкненому шаблоні .[#toc-snippety-vo-vklyucennom-sablone] -===================================================================== - -Може трапитися так, що сніппет міститься в шаблоні, який вмикається з іншого шаблону. У цьому разі необхідно обернути код включення в другому шаблоні макросом `snippetArea`, потім перемалювати як snippetArea, так і сам сніппет. - -Макрос `snippetArea` гарантує, що код усередині нього буде виконано, але браузеру буде надіслано тільки фактичний фрагмент включеного шаблону. +При вставці іншого шаблону в основний за допомогою тегу `{include}`, який містить фрагменти, необхідно знову загорнути включений шаблон в `snippetArea` і зробити недійсним як фрагмент, так і область разом: ```latte -{* parent.latte *} -{snippetArea wrapper} - {include 'child.latte'} +{snippetArea include} + {include 'included.latte'} {/snippetArea} ``` + ```latte -{* child.latte *} +{* included.latte *} {snippet item} -... + ... {/snippet} ``` + ```php -$this->redrawControl('wrapper'); +$this->redrawControl('include'); $this->redrawControl('item'); ``` -Ви також можете поєднувати його з динамічними сніпетами. +Сніппети в компонентах .[#toc-snippets-in-components] +----------------------------------------------------- -Додавання та видалення .[#toc-dobavlenie-i-udalenie] -==================================================== - -Якщо додати новий елемент у список і анулювати `itemsContainer`, AJAX-запит поверне фрагменти, включно з новим, але javascript-обробник не зможе його відобразити. Це відбувається тому, що немає HTML-елемента з новоствореним ID. - -У цьому випадку найпростіший спосіб - обернути весь список у ще один сніпет і визнати його недійсним: +Ви можете створювати фрагменти всередині [компонентів |components], і Nette автоматично перемальовуватиме їх. Однак є певне обмеження: для перемальовування фрагментів він викликає метод `render()` без жодних параметрів. Таким чином, передача параметрів у шаблоні не спрацює: ```latte -{snippet wholeList} - -{/snippet} -Добавить +OK +{control productGrid} + +will not work: +{control productGrid $arg, $arg} +{control productGrid:paginator} ``` + +Надсилання даних користувача .[#toc-sending-user-data] +------------------------------------------------------ + +Разом зі сніппетами ви можете надсилати клієнту будь-які додаткові дані. Просто впишіть їх в об'єкт `payload`: + ```php -public function handleAdd(): void +public function actionDelete(int $id): void { - $this->template->list = $this->getTheWholeList(); - $this->template->list[] = 'New one'; - $this->redrawControl('wholeList'); + //... + if ($this->isAjax()) { + $this->payload->message = 'Success'; + } } ``` -Те ж саме стосується і видалення елемента. Можна було б надіслати порожній сніппет, але зазвичай списки можуть бути посторінковими, і було б складно реалізувати видалення одного елемента і завантаження іншого (який раніше перебував на іншій сторінці посторінкового списку). - -Надсилання параметрів компоненту .[#toc-otpravka-parametrov-komponentu] -======================================================================= +Параметри відправки .[#toc-sending-parameters] +============================================== -Коли ми надсилаємо параметри компоненту через AJAX-запит, чи то сигнальні, чи постійні параметри, ми повинні надати їхнє глобальне ім'я, яке також містить ім'я компонента. Повне ім'я параметра повертає метод `getParameterId()`. +Коли ми надсилаємо параметри компоненту за допомогою AJAX-запиту, будь то сигнальні параметри або постійні параметри, ми повинні вказати їх глобальне ім'я, яке також містить ім'я компонента. Повне ім'я параметра повертає метод `getParameterId()`. ```js -$.getJSON( - {link changeCountBasket!}, - { - {$control->getParameterId('id')}: id, - {$control->getParameterId('count')}: count - } -}); +let url = new URL({link //foo!}); +url.searchParams.set({$control->getParameterId('bar')}, bar); + +fetch(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, +}) ``` -І обробити метод з відповідними параметрами в компоненті. +Метод обробки з відповідними параметрами в компоненті: ```php -public function handleChangeCountBasket(int $id, int $count): void +public function handleFoo(int $bar): void { - } ``` diff --git a/application/uk/bootstrap.texy b/application/uk/bootstrap.texy index f71b35252b..ef7eedd1a7 100644 --- a/application/uk/bootstrap.texy +++ b/application/uk/bootstrap.texy @@ -20,18 +20,44 @@ use Nette\Bootstrap\Configurator; class Bootstrap { - public static function boot(): Configurator + 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 { - $appDir = dirname(__DIR__); - $configurator = new Configurator; - //$configurator->setDebugMode('secret@23.75.345.200'); - $configurator->enableTracy($appDir . '/log'); - $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() + // Nette розумний, і режим розробки вмикається автоматично, + // або ви можете увімкнути його для певної IP-адреси, не коментуючи наступний рядок: + // $this->configurator->setDebugMode('secret@23.75.345.200'); + + // Вмикає Tracy: найкращий інструмент налагодження "швейцарський армійський ніж". + $this->configurator->enableTracy($this->rootDir . '/log'); + + // RobotLoader: автозавантаження всіх класів у вказаному каталозі + $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); - $configurator->addConfig($appDir . '/config/common.neon'); - return $configurator; + } + + private function setupContainer(): void + { + // Завантажити конфігураційні файли + $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } ``` @@ -40,16 +66,15 @@ class Bootstrap index.php .[#toc-index-php] =========================== -У випадку веб-додатків початковим файлом є `index.php`, який знаходиться у загальнодоступному каталозі `www/`. Він дозволяє класу `Bootstrap` ініціалізувати середовище і повертає `$configurator`, який створює контейнер DI. Потім він отримує сервіс `Application`, який запускає веб-додаток: +У випадку веб-додатків основним файлом є `index.php`, який знаходиться у [загальнодоступному каталозі |directory-structure#public-directory-www] `www/`. Клас Bootstrap ініціалізує середовище і створює контейнер DI. Потім він отримує з нього сервіс `Application`, який запускає веб-додаток: ```php -// ініціалізуємо середовище + отримуємо об'єкт Configurator -$configurator = App\Bootstrap::boot(); -// створюємо DI-контейнер -$container = $configurator->createContainer(); -// DI-контейнер створює об'єкт Nette\Application\Application +$bootstrap = new App\Bootstrap; +// Ініціалізація середовища + створення контейнера DI +$container = $bootstrap->bootWebApplication(); +// Контейнер DI створює об'єкт Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -// запускаємо додаток Nette +// Запустіть додаток Nette та обробіть вхідний запит $application->run(); ``` @@ -59,26 +84,42 @@ $application->run(); Режим розробки та режим виробництва .[#toc-development-vs-production-mode] ========================================================================== -Nette розрізняє два основні режими, в яких виконується запит: розробка і виробництво. Режим розробки орієнтований на максимальну зручність програміста, відображається Tracy, кеш автоматично оновлюється у разі зміни шаблонів або конфігурації DI контейнера тощо. Режим виробництва орієнтований на продуктивність, Tracy тільки реєструє помилки, а зміни шаблонів та інших файлів не перевіряються. +Nette поводиться по-різному залежно від того, чи запущено його на сервері розробки, чи на робочому сервері: + +🛠️ Режим розробки: + - Відображає панель налагодження Tracy з корисною інформацією (наприклад, SQL-запити, час виконання, використання пам'яті). + - Показує детальну сторінку помилок з трасуванням викликів функцій та вмістом змінних при виникненні помилки. + - Автоматично оновлює кеш при зміні шаблонів Latte, конфігураційних файлів тощо. + + +🚀 Виробничий режим: + - Не відображає ніякої налагоджувальної інформації; всі помилки реєструються. + - Показує `ErrorPresenter` або загальну сторінку "Помилка сервера", коли виникає помилка. + - Кеш ніколи не оновлюється автоматично! + - Оптимізовано для швидкості та безпеки. -Вибір режиму здійснюється шляхом автовизначення, тому зазвичай немає необхідності налаштовувати або перемикати що-небудь вручну. Режим розробки використовується, якщо застосунок запущено на localhost (тобто IP-адресу `127.0.0.1` або `::1`) і відсутній проксі-сервер (тобто його HTTP-заголовок). В іншому разі застосунок працює у виробничому режимі. + +Режим визначається автоматично, тому в більшості випадків немає необхідності налаштовувати або перемикати його вручну: + +- Режим розробки: Активний на localhost (IP-адреса `127.0.0.1` або `::1`), якщо не використовується проксі (тобто на основі його HTTP-заголовків). +- Виробничий режим: Активний скрізь. Якщо ви хочете ввімкнути режим розробки в інших випадках, наприклад, для програмістів, які отримують доступ з певної IP-адреси, ви можете використовувати `setDebugMode()`: ```php -$configurator->setDebugMode('23.75.345.200'); // одна або більше IP-адрес +$this->configurator->setDebugMode('23.75.345.200'); // одна або більше IP-адрес ``` Ми безумовно рекомендуємо поєднувати IP-адресу з файлом cookie. Ми зберігатимемо секретний токен у cookie `nette-debug', например, `secret1234`, і режим розробки буде активовано для програмістів із такою комбінацією IP і cookie. ```php -$configurator->setDebugMode('secret1234@23.75.345.200'); +$this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` Можна повністю вимкнути режим розробника, навіть для localhost: ```php -$configurator->setDebugMode(false); +$this->configurator->setDebugMode(false); ``` Зверніть увагу, що значення `true` жорстко вмикає режим розробника, чого ніколи не повинно відбуватися на робочому сервері. @@ -90,7 +131,7 @@ $configurator->setDebugMode(false); Для полегшення налагодження ми увімкнемо чудовий інструмент [Tracy |tracy:]. У режимі розробника він візуалізує помилки, а в режимі виробництва - записує помилки в зазначений каталог: ```php -$configurator->enableTracy($appDir . '/log'); +$this->configurator->enableTracy($this->rootDir . '/log'); ``` @@ -100,7 +141,7 @@ $configurator->enableTracy($appDir . '/log'); Nette використовує кеш для DI-контейнера, RobotLoader, шаблонів тощо. Тому необхідно задати шлях до директорії, де зберігатиметься кеш: ```php -$configurator->setTempDirectory($appDir . '/temp'); +$this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` У Linux або macOS встановіть [права на запис |nette:troubleshooting#Setting-Directory-Permissions] для каталогів `log/` і `temp/`. @@ -112,7 +153,7 @@ RobotLoader .[#toc-robotloader] Зазвичай ми хочемо автоматично завантажувати класи за допомогою [RobotLoader |robot-loader:], тому ми повинні запустити його і дозволити йому завантажити класи з каталогу, в якому знаходиться `Bootstrap.php` (тобто `__DIR__`) і всі його підкаталоги: ```php -$configurator->createRobotLoader() +$this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); ``` @@ -126,7 +167,7 @@ $configurator->createRobotLoader() Configurator дає змогу вказати часовий пояс для вашого застосунку. ```php -$configurator->setTimeZone('Europe/Prague'); +$this->configurator->setTimeZone('Europe/Prague'); ``` @@ -143,16 +184,17 @@ $configurator->setTimeZone('Europe/Prague'); Файли конфігурації завантажуються за допомогою `addConfig()`: ```php -$configurator->addConfig($appDir . '/config/common.neon'); +$this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` Метод `addConfig()` може бути викликаний кілька разів для додавання декількох файлів. ```php -$configurator->addConfig($appDir . '/config/common.neon'); -$configurator->addConfig($appDir . '/config/local.neon'); +$configDir = $this->rootDir . '/config'; +$this->configurator->addConfig($configDir . '/common.neon'); +$this->configurator->addConfig($configDir . '/services.neon'); if (PHP_SAPI === 'cli') { - $configurator->addConfig($appDir . '/config/cli.php'); + $this->configurator->addConfig($configDir . '/cli.php'); } ``` @@ -169,7 +211,7 @@ if (PHP_SAPI === 'cli') { Параметри, що використовуються у файлах конфігурації, можуть бути визначені [в секції `parameters` |dependency-injection:configuration#parameters] і підхоплені (або перезаписані) методом `addStaticParameters()` (у нього є аліас `addParameters()`). Важливо, що різні значення параметрів викликають генерацію додаткових DI-контейнерів, тобто додаткових класів. ```php -$configurator->addStaticParameters([ +$this->configurator->addStaticParameters([ 'projectId' => 23, ]); ``` @@ -183,7 +225,7 @@ $configurator->addStaticParameters([ Можна також додати динамічні параметри в контейнер. Їхні різні значення, на відміну від статичних параметрів, не призведуть до генерації нових DI-контейнерів. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'remoteIp' => $_SERVER['REMOTE_ADDR'], ]); ``` @@ -191,7 +233,7 @@ $configurator->addDynamicParameters([ Змінні середовища можуть бути легко доступні з використанням динамічних параметрів. Ми можемо отримати доступ до них через `%env.variable%` у файлах конфігурації. ```php -$configurator->addDynamicParameters([ +$this->configurator->addDynamicParameters([ 'env' => getenv(), ]); ``` @@ -206,6 +248,7 @@ $configurator->addDynamicParameters([ - `%wwwDir%` - абсолютний шлях до каталогу, в якому знаходиться файл запису `index.php` - `%tempDir%` - абсолютний шлях до каталогу для тимчасових файлів - `%vendorDir%` - абсолютний шлях до каталогу, куди Composer встановлює бібліотеки +- `%rootDir%` - абсолютний шлях до кореневого каталогу проекту - `%debugMode%` вказує на те, чи перебуває програма у режимі налагодження - `%consoleMode%` вказує на те, що запит надійшов через командний рядок @@ -225,7 +268,7 @@ services: Створюємо новий екземпляр і вставляємо його в Bootstrap: ```php -$configurator->addServices([ +$this->configurator->addServices([ 'myservice' => new App\Model\MyCustomService('foobar'), ]); ``` @@ -234,13 +277,21 @@ $configurator->addServices([ Різні середовища .[#toc-different-environments] =============================================== -Не соромтеся налаштувати клас `Bootstrap` відповідно до ваших потреб. Ви можете додавати параметри до методу `boot()` для розділення веб-проєктів, або додавати інші методи, як-от `bootForTests()`, які ініціалізують середовище для модульних тестів, `bootForCli()` для скриптів, що викликаються з командного рядка, і так далі. +Не соромтеся налаштовувати клас `Bootstrap` відповідно до ваших потреб. Ви можете додати параметри до методу `bootWebApplication()`, щоб розрізняти веб-проекти. Крім того, ви можете додати інші методи, такі як `bootTestEnvironment()` для ініціалізації середовища для модульних тестів, `bootConsoleApplication()` для скриптів, що викликаються з командного рядка, і так далі. ```php -public static function bootForTests(): Configurator +public function bootTestEnvironment(): Nette\DI\Container +{ + Tester\Environment::setup(); // Ініціалізація Nette Tester + $this->setupContainer(); + return $this->configurator->createContainer(); +} + +public function bootConsoleApplication(): Nette\DI\Container { - $configurator = self::boot(); - Tester\Environment::setup(); // Инициализация Nette Tester - return $configurator; + $this->configurator->setDebugMode(false); + $this->initializeEnvironment(); + $this->setupContainer(); + return $this->configurator->createContainer(); } ``` diff --git a/application/uk/components.texy b/application/uk/components.texy index 834fb55a2f..b8b4c592e4 100644 --- a/application/uk/components.texy +++ b/application/uk/components.texy @@ -230,6 +230,28 @@ $this->redirect(/* ... */); // робимо редирект ``` +Перенаправлення за сигналом .[#toc-redirection-after-a-signal] +============================================================== + +Після обробки сигналу компонента часто відбувається перенаправлення. Ця ситуація схожа на форми - після відправлення форми ми також виконуємо перенаправлення, щоб запобігти повторному відправленню даних при оновленні сторінки в браузері. + +```php +$this->redirect('this') // redirects to the current presenter and action +``` + +Оскільки компонент є елементом багаторазового використання і зазвичай не повинен мати прямої залежності від конкретних доповідачів, методи `redirect()` і `link()` автоматично інтерпретують параметр як сигнал компонента: + +```php +$this->redirect('click') // redirects to the 'click' signal of the same component +``` + +Якщо вам потрібно перенаправити на іншого доповідача або дію, ви можете зробити це через доповідача: + +```php +$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action +``` + + Постійні параметри .[#toc-persistent-parameters] ================================================ @@ -347,7 +369,7 @@ services: Нарешті, ми будемо використовувати цю фабрику в нашому презентері: ```php -class PollPresenter extends Nette\UI\Application\Presenter +class PollPresenter extends Nette\Application\UI\Presenter { public function __construct( private PollControlFactory $pollControlFactory, @@ -380,7 +402,7 @@ interface PollControlFactory Компоненти в Nette Application - це багаторазово використовувані частини веб-додатка, які ми вбудовуємо в сторінки, про що і піде мова в цьому розділі. Які можливості такого компонента? 1) він може бути відображений у шаблоні -2) він знає, яку частину себе відображати під час [AJAX-запиту |ajax#invalidation] (сніпети) +2) він знає, [яку частину себе |ajax#snippets] рендерити під час AJAX-запиту (фрагменти) 3) має можливість зберігати свій стан в URL (постійні параметри) 4) має можливість реагувати на дії користувача (сигнали) 5) створює ієрархічну структуру (де коренем є ведучий) diff --git a/application/uk/configuration.texy b/application/uk/configuration.texy index 650875ec58..7bca0f31a1 100644 --- a/application/uk/configuration.texy +++ b/application/uk/configuration.texy @@ -13,11 +13,15 @@ application: # відображає вкладку "Nette Application" на синьому екрані Tracy? debugger: ... # (bool) за замовчуванням true - # чи буде викликатися презентер помилок у разі помилки? - catchExceptions: ... # (bool) за замовчуванням true на "бойовому" сервері + # чи буде викликано error-presenter при помилці? + # має ефект тільки в режимі розробника + catchExceptions: ... # (bool) за замовчуванням дорівнює true # ім'я презентера помилок - errorPresenter: Error # (string) за замовчуванням 'Nette:Error' + errorPresenter: Error # (string|array) за замовчуванням 'Nette:Error' + + # визначає псевдоніми для доповідачів та подій + aliases: ... # визначає правила для перетворення імені ведучого в клас mapping: ... @@ -27,11 +31,20 @@ application: silentLinks: ... # (bool) за замовчуванням false ``` -Оскільки в режимі розробки презентери помилок за замовчуванням не викликаються, а помилки відображаються Tracy, зміна значення `catchExceptions` на `true` допомагає перевірити коректність роботи презентерів помилок під час розробки. +Починаючи з версії 3.2 `nette/application` можна визначити пару презентувальників помилок: + +```neon +application: + errorPresenter: + 4xx: Error4xx # для виключення Nette\Application\BadRequestException + 5xx: Error5xx # для інших винятків +``` Опція `silentLinks` визначає, як Nette поводиться в режимі розробника, коли генерація посилань не вдається (наприклад, через відсутність презентера тощо). Значення за замовчуванням `false` означає, що Nette запускає `E_USER_WARNING`. Встановлення значення `true` пригнічує це повідомлення про помилку. У виробничому середовищі завжди викликається `E_USER_WARNING`. Ми також можемо вплинути на цю поведінку, встановивши змінну презентера [$invalidLinkMode |creating-links#Invalid-Links]. -Карта [mapping визначає правила |modules#Mapping], за якими ім'я класу виводиться з імені ведучого. +[Псевдоніми спрощують посилання |creating-links#aliases] на часто використовуваних доповідачів. + +[Відображення визначає правила |directory-structure#Presenter Mapping], за якими ім'я класу виводиться з імені доповідача. Автореєстрація презентерів .[#toc-automatic-registration-of-presenters] @@ -82,6 +95,9 @@ latte: # включає [перевірку згенерованого коду |latte:develop#Checking Generated Code] phpLinter: ... # (string) за замовчуванням null + # встановлює локаль + locale: cs_CZ # (string) за замовчуванням null + # клас $this->template templateClass: App\MyTemplateClass # за замовчуванням Nette\Bridges\ApplicationLatte\DefaultTemplate ``` @@ -91,7 +107,7 @@ latte: ```neon latte: расширения: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` /--comment diff --git a/application/uk/creating-links.texy b/application/uk/creating-links.texy index 34ec0457fb..5b0eba478f 100644 --- a/application/uk/creating-links.texy +++ b/application/uk/creating-links.texy @@ -38,7 +38,7 @@ подробнее ``` -Якщо метод `ProductPresenter::renderShow()` не має `$lang` у своїй сигнатурі, він може прочитати значення параметра, використовуючи `$lang = $this->getParameter('lang')`. +Якщо метод `ProductPresenter::renderShow()` не має `$lang` у своїй сигнатурі, він може отримати значення параметра за допомогою `$lang = $this->getParameter('lang')` або з [властивості |presenters#Request Parameters]. Якщо параметри зберігаються в масиві, їх можна розширити за допомогою оператора `(expand)` (щось на зразок оператора `...` у PHP, але працює з асоціативними масивами): @@ -103,7 +103,7 @@ $url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); главная страница ``` -Посилання можуть також вказувати на інші [модулі |modules]. Тут посилання розрізняються на відносні по відношенню до підмодулів або абсолютні. Принцип аналогічний дисковим шляхам, тільки замість косих рисок стоять двокрапки. Припустимо, що справжній презентер є частиною модуля `Front`, тоді ми напишемо: +Посилання можуть також вказувати на інші [модулі |directory-structure#Presenters and Templates]. Тут посилання поділяються на відносні до підмодулів або абсолютні. Принцип аналогічний дисковим шляхам, тільки замість косих рисок ставиться двокрапка. Припустимо, що власне доповідач є частиною модуля `Front`, тоді будемо писати: ```latte ссылка на Front:Shop:Product:show @@ -140,7 +140,7 @@ $url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); обновить ``` -При цьому передаються всі параметри, зазначені в сигнатурі методу `render()` або `action()`. Таким чином, якщо ми перебуваємо на сторінках `Product:show` і `id:123`, посилання на `this` також передаватиме цей параметр. +При цьому всі параметри, зазначені в підписі до файлу `action()` або `render()` якщо метод `action()` не визначено, передаються. Отже, якщо ми знаходимося на сторінках `Product:show` і `id:123`, то посилання на `this` також передасть цей параметр. Звичайно, можна вказати параметри безпосередньо: @@ -213,7 +213,7 @@ $url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); Якщо ми хочемо зробити посилання на презентери в шаблоні компонента, ми використовуємо тег `{plink}`: ```latte -главная страница +главная страница ``` or in the code @@ -223,6 +223,30 @@ $this->getPresenter()->link('Home:default') ``` +Псевдоніми .[#toc-aliases]{data-version:v3.2.2} +=============================================== + +Іноді корисно присвоїти парі доповідач:дія псевдонім, який легко запам'ятовується. Наприклад, ви можете назвати домашню сторінку `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 +``` + +У посиланнях вони пишуться за допомогою символу at, наприклад: + +```latte +administration +``` + +Вони підтримуються у всіх методах, які працюють з посиланнями, таких як `redirect()` та подібних. + + Недійсні посилання .[#toc-invalid-links] ======================================== @@ -257,6 +281,6 @@ LinkGenerator .[#toc-linkgenerator] LinkGenerator - це сервіс, який можна передати через конструктор, а потім створити посилання за допомогою методу 'link()'. -Є різниця порівняно з презентерами. LinkGenerator створює всі посилання як абсолютні URL-адреси. Крім того, немає "поточного презентера", тому неможливо вказати тільки ім'я дії 'link('default')' або відносні шляхи до модулів. +Якщо порівнювати з презентерами, то є різниця. LinkGenerator створює всі посилання безпосередньо як абсолютні URL-адреси. Крім того, тут немає "власне презентера", тому ви не можете просто вказати ім'я дії `link('default')` як ціль або перерахувати відносні шляхи до модулів. Неприпустимі посилання завжди викидають виняток `Nette\Application\UI\InvalidLinkException`. diff --git a/application/uk/directory-structure.texy b/application/uk/directory-structure.texy new file mode 100644 index 0000000000..15a750bd91 --- /dev/null +++ b/application/uk/directory-structure.texy @@ -0,0 +1,526 @@ +Структура каталогу програми +*************************** + +
    + +Як спроектувати чітку та масштабовану структуру каталогів для проектів на Nette Framework? Ми покажемо вам перевірені практики, які допоможуть вам організувати ваш код. Ви навчитеся: + +- як **логічно структурувати додаток в каталоги +- як спроектувати структуру так, щоб вона добре масштабувалася по мірі зростання проекту +- які існують **можливі альтернативи** та їхні переваги чи недоліки + +
    + + +Важливо зазначити, що Nette Framework сам по собі не наполягає на якійсь конкретній структурі. Він розроблений таким чином, щоб його можна було легко адаптувати до будь-яких потреб та вподобань. + + +Базова структура проекту .[#toc-basic-project-structure] +======================================================== + +Хоча Nette Framework не диктує фіксованої структури каталогів, існує перевірена структура за замовчуванням у вигляді [веб-проекту |https://github.com/nette/web-project]: + +/--pre +web-project/ +├── app/ ← каталог програми +├── assets/ ← SCSS, JS файли, зображення..., або ресурси/ +├── bin/ ← скрипти командного рядка +├── config/ ← конфігурація +├── log/ ← зареєстровані помилки +├── temp/ ← тимчасові файли, кеш +├── tests/ ← тести +├── vendor/ ← бібліотеки, встановлені Composer +└── www/ ← загальнодоступний каталог (корінь документа) +\-- + +Ви можете вільно змінювати цю структуру відповідно до ваших потреб - перейменовувати або переміщувати теки. Потім вам потрібно лише відкоригувати відносні шляхи до каталогів в `Bootstrap.php` і, можливо, `composer.json`. Більше нічого не потрібно, ніякого складного переналаштування, ніяких постійних змін. Nette має розумне автовизначення і автоматично розпізнає місцезнаходження програми, включаючи її базу URL-адрес. + + +Принципи організації коду .[#toc-code-organization-principles] +============================================================== + +Коли ви вперше вивчаєте новий проект, ви повинні мати можливість швидко зорієнтуватися. Уявіть, що ви натискаєте на каталог `app/Model/` і бачите таку структуру: + +/--pre +app/Model/ +├── Services/ +├── Repositories/ +└── Entities/ +\-- + +З неї ви дізнаєтеся лише, що проект використовує деякі сервіси, сховища та сутності. Ви нічого не дізнаєтесь про справжню мету програми. + +Давайте розглянемо інший підхід - **організація за доменами**: + +/--pre +app/Model/ +├── Cart/ +├── Payment/ +├── Order/ +└── Product/ +\-- + +Тут все інакше - з першого погляду зрозуміло, що це сайт електронної комерції. Самі назви каталогів показують, що може робити додаток - він працює з платежами, замовленнями і товарами. + +Перший підхід (організація за типами класів) на практиці створює кілька проблем: логічно пов'язаний код розкиданий по різних папках і доводиться перестрибувати між ними. Тому ми будемо організовувати за доменами. + + +Простори імен .[#toc-namespaces] +-------------------------------- + +Прийнято вважати, що структура каталогів відповідає просторам імен у програмі. Це означає, що фізичне розташування файлів відповідає їх простору імен. Наприклад, клас, розташований у каталозі `app/Model/Product/ProductRepository.php`, повинен мати простір імен `App\Model\Product`. Цей принцип допомагає в орієнтації коду і спрощує автозавантаження. + + +Однина та множина в іменах .[#toc-singular-vs-plural-in-names] +-------------------------------------------------------------- + +Зверніть увагу, що ми використовуємо однину для основних каталогів програми: `app`, `config`, `log`, `temp`, `www`. Те ж саме стосується і всередині програми: `Model`, `Core`, `Presentation`. Це тому, що кожна з них представляє одну уніфіковану концепцію. + +Аналогічно, `app/Model/Product` представляє все, що стосується продуктів. Ми не називаємо його `Products`, тому що це не папка, повна продуктів (яка б містила файли типу `iphone.php`, `samsung.php`). Це простір імен, що містить класи для роботи з продуктами - `ProductRepository.php`, `ProductService.php`. + +Папка `app/Tasks` є множинною, тому що містить набір окремих виконуваних скриптів - `CleanupTask.php`, `ImportTask.php`. Кожен з них є самостійною одиницею. + +Для узгодженості рекомендуємо використовувати: +- однину для просторів імен, що представляють функціональну одиницю (навіть якщо ви працюєте з декількома сутностями) +- Множину для колекцій незалежних одиниць +- У випадку невизначеності або якщо ви не хочете про це думати, обирайте однину + + +Публічний каталог `www/` .[#toc-public-directory-www] +===================================================== + +Це єдиний каталог, доступний з Інтернету (так званий корінь документа). Ви можете часто зустріти назву `public/` замість `www/` - це лише питання умовності і не впливає на функціональність. Каталог містить: +- [Точка входу |bootstrap#index.php] програми `index.php` +- `.htaccess` файл з правилами mod_rewrite (для Apache) +- Статичні файли (CSS, JavaScript, зображення) +- Завантажені файли + +Для належної безпеки додатку дуже важливо мати правильно [налаштований корінь документа |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url]. + +.[note] +Ніколи не розміщуйте в цьому каталозі папку `node_modules/` - вона містить тисячі файлів, які можуть бути виконуваними і не повинні бути загальнодоступними. + + +Каталог додатків `app/` .[#toc-application-directory-app] +========================================================= + +Це основний каталог з кодом програми. Базова структура: + +/--pre +app/ +├── Core/ ← питання інфраструктури +├── Model/ ← бізнес-логіка +├── Presentation/ ← презентатори та шаблони +├── Tasks/ ← командні скрипти +└── Bootstrap.php ← завантажувальний клас додатку +\-- + +`Bootstrap.php` [клас запуску програми |bootstrap], який ініціалізує середовище, завантажує конфігурацію та створює контейнер DI. + +Тепер давайте детально розглянемо окремі підкаталоги. + + +Презентації та шаблони .[#toc-presenters-and-templates] +======================================================= + +Презентаційна частина програми знаходиться в каталозі `app/Presentation`. Альтернативою є коротка `app/UI`. Це місце для всіх презентаторів, їхніх шаблонів і будь-яких допоміжних класів. + +Ми організовуємо цей рівень за доменами. У складному проекті, який поєднує в собі електронну комерцію, блог і 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, карти сайту тощо. +\-- + +Теки на кшталт `Home/` або `Dashboard/` містять презентації та шаблони. Папки на кшталт `Front/`, `Admin/` або `Api/` називаються **модулями**. Технічно, це звичайні каталоги, які слугують для логічної організації програми. + +Кожна папка з доповідачем містить однойменний доповідач і його шаблони. Наприклад, папка `Dashboard/` містить: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← ведучий +└── default.latte ← шаблон +\-- + +Ця структура каталогів відображається у просторах імен класів. Наприклад, `DashboardPresenter` знаходиться у просторі імен `App\Presentation\Admin\Dashboard` (див. [відображення |#presenter mapping] доповідача): + +```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`. + + +Гнучка розробка структури .[#toc-flexible-structure-development] +---------------------------------------------------------------- + +Однією з найбільших переваг цієї структури є те, наскільки елегантно вона адаптується до зростаючих потреб проекту. Для прикладу візьмемо частину, що генерує XML-стрічки. Спочатку у нас є проста форма: + +/--pre +Export/ +├── ExportPresenter.php ← один ведучий для всіх експортів +├── sitemap.latte ← шаблон для карти сайту +└── feed.latte ← шаблон для RSS-стрічки +\-- + +Згодом додається більше типів фідів, і нам потрібно більше логіки для них... Ніяких проблем! Папка `Export/` просто стає модулем: + +/--pre +Export/ +├── Sitemap/ +│ ├── SitemapPresenter.php +│ └── sitemap.latte +└── Feed/ + ├── FeedPresenter.php + ├── amazon.latte ← корм для Amazon + └── ebay.latte ← feed для eBay +\-- + +Ця трансформація є абсолютно плавною - просто створіть нові підпапки, розділіть в них код і оновіть посилання (наприклад, з `Export:feed` на `Export:Feed:amazon`). Завдяки цьому ми можемо поступово розширювати структуру в міру необхідності, рівень вкладеності ніяк не обмежений. + +Наприклад, якщо в адміністрації у вас багато презентаторів, пов'язаних з управлінням замовленнями, таких як `OrderDetail`, `OrderEdit`, `OrderDispatch` і т.д., ви можете створити модуль (папку) `Order` для кращої організації, в якому будуть міститися (папки для) презентаторів `Detail`, `Edit`, `Dispatch` та інші. + + +Розташування шаблону .[#toc-template-location] +---------------------------------------------- + +У попередніх прикладах ми бачили, що шаблони знаходяться безпосередньо в папці з доповідачем: + +/--pre +Dashboard/ +├── DashboardPresenter.php ← ведучий +├── DashboardTemplate.php ← необов'язковий клас шаблону +└── default.latte ← шаблон +\-- + +Таке розташування виявляється найзручнішим на практиці - всі пов'язані файли у вас під рукою. + +Крім того, ви можете розмістити шаблони в підпапці `templates/`. Nette підтримує обидва варіанти. Ви навіть можете розмістити шаблони поза текою `Presentation/`. Все про варіанти розташування шаблонів ви можете знайти в розділі [Пошук шаблонів |templates#Template Lookup]. + + +Допоміжні класи та компоненти .[#toc-helper-classes-and-components] +------------------------------------------------------------------- + +Презентації та шаблони часто супроводжуються іншими допоміжними файлами. Ми розмістили їх логічно відповідно до їхньої сфери застосування: + +1. **Безпосередньо з доповідачем** у випадку специфічних компонентів для даного доповідача: + +/--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`. Вибір залежить від конвенцій команди. + + +Модель - серце програми .[#toc-model-heart-of-the-application] +============================================================== + +Модель містить всю бізнес-логіку додатку. Для її організації застосовується те саме правило - структуруємо за доменами: + +/--pre +app/Model/ +├── Payment/ ← все про платежі +│ ├── PaymentFacade.php ← основна точка входу +│ ├── PaymentRepository.php +│ ├── Payment.php ← суб'єкт +├── Order/ ← все про замовлення +│ ├── OrderFacade.php +│ ├── OrderRepository.php +│ ├── Order.php +└── Shipping/ ← все про доставку +\-- + +У моделі зазвичай зустрічаються такі типи класів: + +**Фасади**: представляють основну точку входу в певний домен у додатку. Вони виступають в ролі оркестратора, який координує співпрацю між різними сервісами для реалізації повних варіантів використання (наприклад, "створити замовлення" або "обробити платіж"). Під своїм оркестровим шаром фасад приховує деталі реалізації від решти додатку, забезпечуючи таким чином чистий інтерфейс для роботи з даним доменом. + +```php +class OrderFacade +{ + public function createOrder(Cart $cart): Order + { + // валідація + // створення замовлення + // відправка на електронну пошту + // запис до статистики + } +} +``` + +**Сервіси**: зосереджені на конкретних бізнес-операціях в межах домену. На відміну від фасадів, які організовують цілі сценарії використання, сервіс реалізує конкретну бізнес-логіку (наприклад, розрахунок ціни або обробку платежів). Сервіси, як правило, не мають статусу і можуть використовуватися або фасадами як будівельні блоки для більш складних операцій, або безпосередньо іншими частинами програми для простіших завдань. + +```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 +// Сутність зіставлена з таблицею замовлень бази даних +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, + ]); + } +} +``` + +**Об'єкти-значення**: незмінні об'єкти, що представляють значення без власної ідентичності - наприклад, сума грошей або адреса електронної пошти. Два екземпляри об'єкта-значення з однаковими значеннями вважаються ідентичними. + + +Інфраструктурний код .[#toc-infrastructure-code] +================================================ + +Папка `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` для відправки листів про замовлення, знає їхні шаблони та час відправки + + +Командні скрипти .[#toc-command-scripts] +======================================== + +Додаткам часто потрібно виконувати завдання поза звичайними 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-запиту, але при цьому слід враховувати питання безпеки. Доповідач, який запускає завдання, повинен бути захищений, наприклад, тільки для зареєстрованих користувачів або за допомогою надійного токена і доступу з дозволених IP-адрес. Для довгих завдань необхідно збільшити ліміт часу скрипта і використовувати `session_write_close()`, щоб уникнути блокування сеансу. + + +Інші можливі директорії .[#toc-other-possible-directories] +========================================================== + +На додаток до згаданих базових каталогів, ви можете додати інші спеціалізовані папки відповідно до потреб проекту. Розглянемо найпоширеніші з них та їх використання: + +/--pre +app/ +├── Api/ ← Логіка API не залежить від рівня представлення +├── Database/ ← скрипти міграції та сівалки для тестових даних +├── Components/ ← спільні візуальні компоненти у всьому додатку +├── Event/ ← корисно, якщо використовується архітектура, керована подіями +├── Mail/ ← шаблони листів та пов'язана з ними логіка +└── Utils/ ← допоміжні класи +\-- + +Для спільних візуальних компонентів, що використовуються у презентаторах у всьому додатку, ви можете використовувати папку `app/Components` або `app/Controls`: + +/--pre +app/Components/ +├── Form/ ← компоненти спільних форм +│ ├── SignInForm.php +│ └── UserForm.php +├── Grid/ ← компоненти для списків даних +│ └── DataGrid.php +└── Navigation/ ← елементи навігації + ├── Breadcrumbs.php + └── Menu.php +\-- + +Це місце для компонентів зі складнішою логікою. Якщо ви хочете спільно використовувати компоненти між кількома проектами, краще виокремити їх в окремий пакунок композитора. + +У директорії `app/Mail` ви можете розмістити управління комунікацією електронною поштою: + +/--pre +app/Mail/ +├── templates/ ← шаблони листів +│ ├── order-confirmation.latte +│ └── welcome.latte +└── OrderMailer.php +\-- + + +Мапування ведучого .[#toc-presenter-mapping] +============================================ + +Відображення визначає правила отримання імен класів з імен доповідачів. Ми вказуємо їх у [конфігурації |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 +``` + +Відображення працює шляхом заміни зірочки у масці `App\Presentation\*Presenter` на ім'я доповідача `Home`, у результаті чого буде отримано кінцеве ім'я класу `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 +``` + +Це також працює для глибше вкладених структур каталогів, таких як 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 index 03cb3b413a..1036c9ce04 100644 --- a/application/uk/how-it-works.texy +++ b/application/uk/how-it-works.texy @@ -22,18 +22,18 @@ /--pre web-project/ ├── app/ ← каталог с приложением -│ ├── Presenters/ ← классы презентеров -│ │ ├── HomePresenter.php ← Класс презентера главной страницы -│ │ └── templates/ ← директория шаблонов -│ │ ├── @layout.latte ← шаблон общего макета -│ │ └── Home/ ← шаблоны презентера главной страницы -│ │ └── default.latte ← шаблон действия `default` -│ ├── Router/ ← конфигурация URL-адресов +│ ├── Core/ ← основні необхідні класи +│ │ └── RouterFactory.php ← конфігурація URL-адрес +│ ├── Presentation/ ← презентатори, шаблони та інше +│ │ ├── @layout.latte ← шаблон спільного макета +│ │ └── Home/ ← домашній каталог доповідачів +│ │ ├── HomePresenter.php ← клас головного доповідача +│ │ └── default.latte ← шаблон дії default │ └── Bootstrap.php ← загрузочный класс Bootstrap ├── bin/ ← скрипты командной строки ├── config/ ← файлы конфигурации │ ├── common.neon -│ └── local.neon +│ └── services.neon ├── log/ ← журналы ошибок ├── temp/ ← временные файлы, кэш, … ├── vendor/ ← библиотеки, установленные через Composer @@ -45,9 +45,9 @@ └── .htaccess ← запрещает доступ ко всем каталогам, кроме www \-- -Ви можете змінити структуру каталогів будь-яким способом, перейменувати або перемістити папки, а потім просто відредагувати шляхи до `log/` і `temp/` у файлі `Bootstrap.php` і шлях до цього файлу в `composer.json` у секції `autoload`. Нічого більше, жодного складного переналаштування, жодних постійних змін. Nette має [інтелектуальне автовизначення |bootstrap#development-vs-production-mode]. +Ви можете змінювати структуру каталогів як завгодно, перейменовувати або переміщати папки - це абсолютно гнучка система. Nette також має інтелектуальне автовизначення і автоматично розпізнає місцезнаходження програми, включаючи її базу URL-адрес. -Для трохи більших додатків ми можемо розділити папки з ведучими і шаблонами на підкаталоги (на диску) і на простори імен (у коді), які ми називаємо [модулями |modules]. +Для трохи більших додатків ми можемо організувати папки доповідача і шаблонів у [підкаталоги |directory-structure#Presenters and templates] і згрупувати класи у простори імен, які ми називаємо модулями. Публічний каталог `www/` може бути змінений без необхідності встановлювати що-небудь ще. Насправді, часто буває, що через специфіку вашого хостингу вам доведеться перейменувати його або, навпаки, встановити так званий document-root на цей каталог у конфігурації хостингу. Якщо ваш хостинг не дозволяє створювати папки на один рівень вище публічного каталогу, радимо вам пошукати інший хостинг. В іншому разі ви піддасте себе значному ризику безпеки. @@ -75,7 +75,7 @@ HTTP-запит .[#toc-http-request] Що за фабрика? Ми виробляємо не трактори, а веб-сайти! Зачекайте, зараз усе буде пояснено. -Під "ініціалізацією середовища" мається на увазі, наприклад, що активовано сервіс [Tracy |tracy:], який є дивовижним інструментом для реєстрації або візуалізації помилок. Він реєструє помилки на робочому сервері та відображає їх безпосередньо на сервері розробки. Тому під час ініціалізації також необхідно вирішити, чи працює сайт у виробничому режимі або в режимі розробника. Для цього Nette використовує автовизначення: якщо ви запускаєте сайт на localhost, він працює в режимі розробника. Вам не потрібно нічого налаштовувати, і додаток готовий як для розробки, так і для виробничого розгортання. Ці кроки виконуються і детально описуються в розділі [Bootstrap |bootstrap]. +Під "ініціалізацією середовища" ми маємо на увазі, наприклад, активацію [Tracy |tracy:], який є фантастичним інструментом для логування та візуалізації помилок. На виробничих серверах він реєструє помилки, в той час як на розробці він відображає їх безпосередньо. Тому ініціалізація включає в себе визначення того, чи працює веб-сайт в режимі виробництва або розробки. Для цього Nette використовує [розумне автовизначення |bootstrap#development-vs-production-mode]: якщо ви запускаєте сайт на localhost, він працює в режимі розробки. Конфігурація не потрібна, і додаток готовий як для розробки, так і для розгортання у виробництві. Ці кроки виконуються і детально описані в розділі " [Клас Bootstrap |bootstrap] ". Третій пункт (так, ми пропустили другий, але ми до нього повернемося) - це запуск програми. Обробкою HTTP-запитів у Nette займається клас `Nette\Application\Application` (далі `Application`), тому коли ми говоримо "запустити застосунок", ми маємо на увазі виклик методу з ім'ям `run()` на об'єкті цього класу. @@ -91,7 +91,7 @@ Nette - це наставник, який спрямовує вас до нап Додаток починає роботу з того, що просить так званий маршрутизатор вирішити, якому з презентерів передати поточний запит на обробку. Маршрутизатор вирішує, чия це відповідальність. Він переглядає вхідний URL `https://example.com/product/123`, який хоче `показать` продукт із `id: 123` як дію. Доброю звичкою є написання пар презентер + дія, розділених двокрапкою: `Продукт:показать`. -Тому маршрутизатор перетворив URL у пару `Presenter:action` + параметри, у нашому випадку `Product:show` + `id`: 123`. Вы можете увидеть, как выглядит маршрутизатор в файле `app/Router/RouterFactory.php`, і ми детально опишемо його в розділі [Маршрутизація |routing]. +Тому маршрутизатор перетворив URL у пару `Presenter:action` + параметри, у нашому випадку `Product:show` + `id`: 123`. Вы можете увидеть, как выглядит маршрутизатор в файле `app/Core/RouterFactory.php`, і ми детально опишемо його в розділі [Маршрутизація |routing]. Давайте рухатися далі. Додаток уже знає ім'я презентера і може продовжити роботу. Шляхом створення об'єкта `ProductPresenter`, який є кодом презентера `Product`. Точніше, він просить контейнер DI створити презентера, тому що створення об'єктів - це його робота. @@ -121,12 +121,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Після цього презентер повертає відповідь. Це може бути HTML-сторінка, зображення, XML-документ, надсилання файлу з диска, JSON або перенаправлення на іншу сторінку. Важливо зазначити, що якщо ми явно не вказуємо, як реагувати (що має місце у випадку з `ProductPresenter`), відповіддю буде відображення шаблону з HTML-сторінкою. Чому? Ну, тому що в 99% випадків ми хочемо відобразити шаблон, тому презентер приймає таку поведінку за замовчуванням і хоче полегшити нашу роботу. Це точка зору Nette. -Нам навіть не потрібно вказувати, який шаблон потрібно вивести, він сам виводить шлях до нього відповідно до простої логіки. У випадку з презентером `Product` і дією `show`, він намагається перевірити, чи існує один із цих файлів шаблонів відносно каталогу, в якому знаходиться клас `ProductPresenter`: +Нам навіть не потрібно вказувати, який шаблон рендерити; фреймворк сам визначить шлях. У випадку дії `show` він просто намагається завантажити шаблон `show.latte` з каталогу з класом `ProductPresenter`. Він також намагається знайти макет у файлі `@layout.latte` (докладніше про [пошук шаблонів |templates#Template Lookup]). -- `templates/Product/show.latte` -- `templates/Product.show.latte` - -І потім він відображає шаблон. Тепер завдання презентера і всієї програми виконано. Якщо шаблону не існує, буде повернуто сторінку з помилкою 404. Детальніше про презентери ви можете прочитати на сторінці [Презентери |presenters]. +Згодом шаблони візуалізуються. На цьому завдання доповідача і всієї програми завершується, і робота завершується. Якщо шаблон не існує, буде повернута сторінка помилки 404. Ви можете прочитати більше про доповідачів на сторінці [Доповідачі |presenters]. [* request-flow.svg *] @@ -137,7 +134,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter 3) маршрутизатор декодує URL як пару `Home:default` 4) створюється об'єкт `HomePresenter` 5) викликається метод `renderDefault()` (якщо існує) -6) шаблон `templates/Home/default.latte` з макетом `templates/@layout.latte` відмальований +6) шаблон `default.latte` з макетом `@layout.latte` відмальований Можливо, зараз ви зіткнулися з безліччю нових понять, але ми вважаємо, що вони мають сенс. Створювати додатки в Nette - простіше простого. diff --git a/application/uk/modules.texy b/application/uk/modules.texy deleted file mode 100644 index 5503142028..0000000000 --- a/application/uk/modules.texy +++ /dev/null @@ -1,148 +0,0 @@ -Модулі -****** - -.[perex] -У Nette модулі являють собою логічні одиниці, з яких складається додаток. Вони включають ведучі, шаблони, можливо, компоненти та класи моделей. - -Одного компонента для презентаторів і одного для шаблонів буде недостатньо для реальних проектів. Наявність десятків файлів в одній папці щонайменше неорганізована. Як вийти з цього становища? Ми просто розділяємо їх на підкаталоги на диску і на простори імен у коді. І це саме те, що роблять модулі Nette. - -Тому давайте забудемо про єдину папку для ведучих і шаблонів і натомість створимо модулі, наприклад, `Admin` і `Front`. - -/--pre -app/ -├── Presenters/ -├── Modules/ ← директория с модулями -│ ├── Admin/ ← модуль Admin -│ │ ├── Presenters/ ← его презентеры -│ │ │ ├── DashboardPresenter.php -│ │ │ └── templates/ -│ └── Front/ ← модуль Front -│ └── Presenters/ ← его презентеры -│ └── ... -\-- - -Ця структура каталогів буде відображена в просторах імен класів, так, наприклад, `DashboardPresenter` буде знаходитися в просторі `App\Modules\Admin\Presenters`: - -```php -namespace App\Modules\Admin\Presenters; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Ведучий `Dashboard` усередині модуля `Admin` позначається в додатку за допомогою подвійної точкової нотації як `Admin:Dashboard`, а його дія `default` позначається як `Admin:Dashboard:default`. -А звідки Nette знає, що `Admin:Dashboard` представляє клас `App\Modules\Admin\Presenters\DashboardPresenter`? Ми говоримо про це, використовуючи [відображення |#Mapping] в конфігурації. -Таким чином, наведена структура не є фіксованою, і ви можете змінювати її на свій розсуд. - -Модулі, звісно, можуть містити всі інші частини, крім презентаторів і шаблонів, такі як компоненти, класи моделей тощо. - - -Вкладені модулі .[#toc-nested-modules] --------------------------------------- - -Модулі не обов'язково повинні формувати тільки плоску структуру, ви також можете створювати, наприклад, підмодулі: - -/--pre -app/ -├── Modules/ ← директория с модулями -│ ├── Blog/ ← модуль Blog -│ │ ├── Admin/ ← подмодуль Admin -│ │ │ ├── Presenters/ -│ │ │ └── ... -│ │ └── Front/ ← подмодуль Front -│ │ ├── Presenters/ -│ │ └── ... -│ ├── Forum/ ← модуль Forum -│ │ └── ... -\-- - -Таким чином, модуль `Blog` розбивається на підмодулі `Admin` і `Front`. І знову ж таки це буде відображено в просторах імен, які будуть `App\Modules\Blog\Admin\Presenters` тощо. Ведучий `Dashboard` всередині підмодуля називається `Blog:Admin:Dashboard`. - -Розгалуження може бути настільки глибоким, наскільки ви захочете, тому ви можете створювати підмодулі. - - -Створення посилань .[#toc-creating-links] ------------------------------------------ - -Посилання в шаблонах ведучого є відносними щодо поточного модуля. Таким чином, посилання `Foo:default` веде до ведучого `Foo` у тому ж модулі, що й поточний ведучий. Наприклад, якщо поточним модулем є `Front`, то посилання має такий вигляд: - -```latte -odkaz na Front:Product:show -``` - -Посилання є відносним, навіть якщо ім'я модуля є його частиною, тоді він вважається підмодулем: - -```latte -odkaz na Front:Shop:Product:show -``` - -Абсолютні посилання записуються аналогічно абсолютним шляхам на диску, але з двокрапками замість косих рисок. Таким чином, абсолютне посилання починається з двокрапки: - -```latte -odkaz na Admin:Product:show -``` - -Щоб дізнатися, чи перебуваємо ми в певному модулі або підмодулі, ми використовуємо функцію `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Маршрутизація .[#toc-routing] ------------------------------ - -Див. [розділ про маршрутизацію |routing#modules]. - - -Складання карти .[#toc-mapping] -------------------------------- - -Визначає правила, за якими ім'я класу виводиться з імені ведучого. Ми записуємо їх у [конфігурацію |configuration] під ключем `application › mapping`. - -Почнемо з прикладу, в якому не використовуються модулі. Ми просто хочемо, щоб класи ведучого мали простір імен `App\Presenters`. Тобто ми хочемо, щоб ведучий, наприклад, `Home` відображався на клас `App\Presenters\HomePresenter`. Цього можна досягти за допомогою такої конфігурації: - -```neon -application: - mapping: - *: App\Presenters\*Presenter -``` - -Ім'я презентера замінюється зірочкою, і в результаті виходить назва класу. Легко! - -Якщо ми розділимо доповідачів на модулі, то для кожного модуля в нас може бути свій маппінг: - -```neon -application: - mapping: - Front: App\Modules\Front\Presenters\*Presenter - Admin: App\Modules\Admin\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Тепер презентер `Front:Home` визначається класом `App\Modules\Front\HomePresenter`, а презентер `Admin:Dashboard` ` - `App\AdminModule\DashboardPresenter`. - -Зручніше буде створити загальне правило (зірочка), яке замінить перші два правила і додасть додаткову зірочку тільки для модуля: - -```neon -application: - mapping: - *: App\Modules\*\Presenters\*Presenter - Api: App\Api\*Presenter -``` - -Але що якщо ми використовуємо кілька вкладених модулів і в нас є, наприклад, провідний `Admin:User:Edit`? У цьому випадку сегмент із зірочкою, що представляє модуль для кожного рівня, буде просто повторюватися, і результатом буде клас `App\Modules\Admin\User\Presenters\EditPresenter`. - -Альтернативною нотацією є використання масиву, що складається з трьох сегментів, замість рядка. Ця нотація еквівалентна попередній: - -```neon -application: - mapping: - *: [App\Modules, *, Presenters\*Presenter] -``` - -Значення за замовчуванням - `*: *Module\*Presenter`. diff --git a/application/uk/presenters.texy b/application/uk/presenters.texy index 1f81ec0dbf..47e3d7b0c0 100644 --- a/application/uk/presenters.texy +++ b/application/uk/presenters.texy @@ -60,7 +60,7 @@ class ArticlePresenter extends Nette\Application\UI\Presenter Важливо, що `action()` викликається перед `render()`, тому всередині нього ми можемо, можливо, змінити наступний хід життєвого циклу, тобто змінити шаблон, який буде відображатися, а також метод `render()`, який буде викликатися, використовуючи `setView('otherView')`. -У метод передаються параметри із запиту. Можна і рекомендується вказувати типи для параметрів, наприклад `actionShow(int $id, string $slug = null)` - якщо параметр `id` відсутній або якщо він не є цілим числом, презентер повертає [помилку 404 |#Error-404-etc] і завершує операцію. +У метод передаються параметри із запиту. Можна і рекомендується вказувати типи для параметрів, наприклад `actionShow(int $id, ?string $slug = null)` - якщо параметр `id` відсутній або якщо він не є цілим числом, презентер повертає [помилку 404 |#Error-404-etc] і завершує операцію. `handle(args...)` .{toc: handle()} @@ -205,7 +205,7 @@ $this->redirect(/* ... */); Помилка 404 тощо. .[#toc-error-404-etc] ======================================= -Коли ми не можемо виконати запит, тому що, наприклад, стаття, яку ми хочемо відобразити, не існує в базі даних, ми викинемо помилку 404, використовуючи метод `error(string $message = null, int $httpCode = 404)`, який представляє HTTP-помилку 404: +Коли ми не можемо виконати запит, тому що, наприклад, стаття, яку ми хочемо відобразити, не існує в базі даних, ми викинемо помилку 404, використовуючи метод `error(?string $message = null, int $httpCode = 404)`, який представляє HTTP-помилку 404: ```php public function renderShow(int $id): void @@ -236,6 +236,32 @@ public function actionData(): void ``` +Параметри запиту .[#toc-request-parameters] +=========================================== + +Доповідач, як і кожен компонент, отримує свої параметри з HTTP-запиту. Їх значення можна отримати за допомогою методу `getParameter($name)` або `getParameters()`. Значення є рядками або масивами рядків, по суті, необробленими даними, отриманими безпосередньо з URL-адреси. + +Для більшої зручності ми рекомендуємо зробити параметри доступними через властивості. Просто додайте до них анотацію з атрибутом `#[Parameter]` за допомогою атрибута + +```php +use Nette\Application\Attributes\Parameter; // ця лінія важлива + +class HomePresenter extends Nette\Application\UI\Presenter +{ + #[Parameter] + public string $theme; // має бути публічною +} +``` + +Для властивостей ми рекомендуємо вказувати тип даних (наприклад, `string`). Тоді Nette автоматично перетворить значення на основі цього типу. Значення параметрів також можна [перевірити |#Validation of Parameters]. + +При створенні посилання ви можете безпосередньо задати значення параметрів: + +```latte +click +``` + + Постійні параметри .[#toc-persistent-parameters] ================================================ @@ -257,7 +283,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Якщо `$this->lang` має значення `'en'`, то посилання, створені за допомогою `link()` або `n:href`, також будуть містити параметр `lang=en`. І коли посилання буде натиснуто, воно знову стане `$this->lang = 'en'`. -Для властивостей рекомендується вказувати тип даних (наприклад, `string`), а також можна вказати значення за замовчуванням. Значення параметрів можна [перевіряти |#Validation of Persistent Parameters]. +Для властивостей рекомендується вказувати тип даних (наприклад, `string`), а також значення за замовчуванням. Значення параметрів можуть бути [перевірені |#Validation of Parameters]. Постійні параметри за замовчуванням передаються між усіма діями даного доповідача. Щоб передати їх між кількома доповідачами, вам також потрібно їх визначити: @@ -307,18 +333,12 @@ class ProductPresenter extends Nette\Application\UI\Presenter Того, що ми показали досі в цьому розділі, ймовірно, буде достатньо. Наступні рядки призначені для тих, хто цікавиться презентерами досконально і хоче знати все. -Вимоги та параметри .[#toc-requirement-and-parameters] ------------------------------------------------------- +Перевірка параметрів .[#toc-validation-of-parameters] +----------------------------------------------------- -Запит, який обробляє доповідач, є об'єктом [api:Nette\Application\Request] і повертається методом доповідача `getRequest()`. Він містить масив параметрів, кожен з яких належить або якомусь з компонентів, або безпосередньо доповідачу (який, власне, теж є компонентом, хоча й особливим). Тож Nette перерозподіляє параметри та передачі між окремими компонентами (та доповідачем) за допомогою виклику методу `loadState(array $params)`. Параметри можна отримати за допомогою методу `getParameters(): array`, окремо за допомогою `getParameter($name)`. Значення параметрів - це рядки або масиви рядків, в основному це необроблені дані, отримані безпосередньо з URL-адреси. +Значення [параметрів запиту |#request parameters] і [постійних параметрів |#persistent parameters], отриманих з URL-адрес, записуються у властивості методом `loadState()`. Також перевіряється, чи збігається тип даних, вказаний у властивості, інакше буде видано помилку 404 і сторінка не буде відображена. - -Перевірка постійних параметрів .[#toc-validation-of-persistent-parameters] --------------------------------------------------------------------------- - -Значення [постійних параметрів |#persistent parameters], отриманих з URL-адрес, записуються у властивості методом `loadState()`. Також перевіряється, чи збігається тип даних, вказаний у властивості, інакше буде видано помилку 404, і сторінка не буде відображена. - -Ніколи не довіряйте сліпо постійним параметрам, оскільки вони можуть бути легко перезаписані користувачем в URL. Наприклад, так ми перевіряємо, чи є `$this->lang` серед підтримуваних мов. Хороший спосіб зробити це - перевизначити метод `loadState()`, згаданий вище: +Ніколи не довіряйте параметрам наосліп, оскільки вони можуть бути легко переписані користувачем в URL-адресі. Наприклад, так ми перевіряємо, чи є `$this->lang` серед підтримуваних мов. Хороший спосіб зробити це - перевизначити метод `loadState()`, згаданий вище: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -341,7 +361,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter Збереження та відновлення запиту .[#toc-save-and-restore-the-request] --------------------------------------------------------------------- -Ви можете зберегти поточний запит у сесії або відновити його із сесії та дозволити презентеру виконати його знову. Це корисно, наприклад, коли користувач заповнює форму, а термін дії його логіна закінчується. Щоб не втратити дані, перед перенаправленням на сторінку реєстрації ми зберігаємо поточний запит у сесію за допомогою функції `$reqId = $this->storeRequest()`, яка повертає ідентифікатор у вигляді короткого рядка і передає його як параметр презенту для реєстрації. +Запит, який обробляє доповідач, є об'єктом [api:Nette\Application\Request] і повертається методом доповідача `getRequest()`. + +Ви можете зберегти поточний запит в сеансі або відновити його з сеансу і дозволити ведучому виконати його знову. Це корисно, наприклад, коли користувач заповнив форму, а його логін закінчується. Щоб не втратити дані, перед перенаправленням на сторінку входу, ми зберігаємо поточний запит до сесії за допомогою `$reqId = $this->storeRequest()`, який повертає ідентифікатор у вигляді короткого рядка і передає його як параметр ведучому входу. Після входу в систему ми викликаємо метод `$this->restoreRequest($reqId)`, який забирає запит у сесії та пересилає його їй. Метод перевіряє, що запит був створений тим самим користувачем, який зараз увійшов у систему. Якщо інший користувач увійшов у систему або ключ недійсний, він нічого не робить, і програма продовжує роботу. @@ -362,7 +384,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Ви також можете викликати канонізацію вручну за допомогою методу `canonicalize()`, який, як і метод `link()`, отримує як аргументи презентера, дії та параметри. Він створює посилання і порівнює його з поточним URL. Якщо вони відрізняються, то відбувається перенаправлення на згенероване посилання. ```php -public function actionShow(int $id, string $slug = null): void +public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); // перенаправляє, якщо $slug відрізняється від $realSlug @@ -425,6 +447,51 @@ $this->sendResponse(new Responses\CallbackResponse($callback)); ``` +Обмеження доступу за допомогою `#[Requires]` .[#toc-access-restriction-using-requires]{data-version:3.2.2} +---------------------------------------------------------------------------------------------------------- + +Атрибут `#[Requires]` надає розширені можливості для обмеження доступу до доповідачів та їхніх методів. Його можна використовувати для визначення HTTP-методів, вимагати AJAX-запитів, обмежувати доступ до одного і того ж джерела та обмежувати доступ лише пересиланням. Атрибут можна застосовувати до класів презентера, а також до окремих методів, таких як `action()`, `render()`, `handle()`та `createComponent()`. + +Ви можете вказати такі обмеження: +- на HTTP-методи: `#[Requires(methods: ['GET', 'POST'])]` +- що вимагають AJAX-запиту: `#[Requires(ajax: true)]` +- доступ тільки з одного джерела: `#[Requires(sameOrigin: true)]` +- доступ тільки через переадресацію: `#[Requires(forward: true)]` +- обмеження на певні дії: `#[Requires(actions: 'default')]` + +За деталями дивіться [Як використовувати атрибут Requires атрибут |best-practices:attribute-requires]. + + +Перевірка методу HTTP .[#toc-http-method-check] +----------------------------------------------- + +У 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`, ви також повинні правильно обробляти його у вашому презентері. Цей метод часто використовується як так званий попередній запит, який браузери автоматично надсилають перед самим запитом, коли необхідно визначити, чи дозволений запит з точки зору політики CORS (Cross-Origin Resource Sharing). Якщо ви дозволите цей метод, але не реалізуєте відповідну реакцію, це може призвести до невідповідностей і потенційних проблем з безпекою. + + Читати далі .[#toc-further-reading] =================================== diff --git a/application/uk/routing.texy b/application/uk/routing.texy index c19046885b..89ae83fc66 100644 --- a/application/uk/routing.texy +++ b/application/uk/routing.texy @@ -216,7 +216,7 @@ $router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ @@ -225,7 +225,7 @@ $router->addRoute('/[/]', [ ]); ``` -Або ми можемо використовувати цю форму, зверніть увагу на переписування регулярного виразу перевірки: +Для більш детальної специфікації можна використовувати ще більш розширену форму, де на додаток до значень за замовчуванням можна задати інші властивості параметрів, наприклад, регулярний вираз перевірки (див. параметр `id` ): ```php use Nette\Routing\Route; @@ -243,7 +243,7 @@ $router->addRoute('/[/]', [ ]); ``` -Ці детальніші формати корисні для додавання додаткових метаданих. +Важливо відзначити, що якщо параметри, визначені в масиві, не включені в маску шляху, їх значення не можуть бути змінені, навіть за допомогою параметрів запиту, зазначених після знака питання в URL-адресі. Фільтри та переклади .[#toc-filters-and-translations] @@ -368,7 +368,7 @@ $router->addRoute('', function (string $lang) { Модулі .[#toc-modules] ---------------------- -Якщо у нас є кілька маршрутів, що належать одному модулю, ми можемо використовувати `withModule()` для їхнього групування: +Якщо ми маємо більше маршрутів, які належать до одного [модуля |directory-structure#Presenters and Templates], ми можемо використовувати `withModule()`, щоб згрупувати їх: ```php $router = new RouteList; @@ -477,10 +477,10 @@ $router->addRoute('index', /* ... */); Інтеграція .[#toc-integration] ============================== -Щоб підключити наш маршрутизатор до застосунку, ми повинні повідомити про нього контейнер DI. Найпростіший спосіб - це підготувати фабрику, яка буде створювати об'єкт маршрутизатора, і повідомити конфігурацію контейнера, щоб вона його використовувала. Припустимо, ми напишемо для цього метод `App\Router\RouterFactory::createRouter()`: +Щоб підключити наш маршрутизатор до застосунку, ми повинні повідомити про нього контейнер DI. Найпростіший спосіб - це підготувати фабрику, яка буде створювати об'єкт маршрутизатора, і повідомити конфігурацію контейнера, щоб вона його використовувала. Припустимо, ми напишемо для цього метод `App\Core\RouterFactory::createRouter()`: ```php -namespace App\Router; +namespace App\Core; use Nette\Application\Routers\RouteList; @@ -499,7 +499,7 @@ class RouterFactory ```neon services: - - App\Router\RouterFactory::createRouter + - App\Core\RouterFactory::createRouter ``` Будь-які залежності, такі як підключення до бази даних тощо, передаються методу фабрики як параметри за допомогою [autowiring |dependency-injection:autowiring]: @@ -663,7 +663,7 @@ $router->addRoute(/* ... */); Отже, ми знову додамо метод, який буде створювати, наприклад, маршрутизатор: ```php -namespace App\Router; +namespace App\Core; use Nette\Routing\RouteList; @@ -694,7 +694,7 @@ $httpRequest = $container->getByType(Nette\Http\IRequest::class); Або ми будемо створювати об'єкти безпосередньо: ```php -$router = App\Router\RouterFactory::createRouter(); +$router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` diff --git a/application/uk/templates.texy b/application/uk/templates.texy index 2dc5f8f053..18651b86f6 100644 --- a/application/uk/templates.texy +++ b/application/uk/templates.texy @@ -34,35 +34,81 @@ Nette використовує систему шаблонів [Latte |latte:]. Він визначає блок `content`, який вставляється замість `{include content}` у макеті, а також перевизначає блок `title`, який перезаписує `{block title}` у макеті. Спробуйте уявити собі результат. -Пошук шаблонів .[#toc-search-for-templates] -------------------------------------------- +Пошук шаблонів .[#toc-template-lookup] +-------------------------------------- -Шлях до шаблонів визначається ведучим за допомогою простої логіки. Він спробує перевірити, чи є один із цих файлів, розташований відносно каталогу класу ведучого, де `` це ім'я поточного ведучого, а `` це ім'я поточної події: +У презентаторах вам не потрібно вказувати, який шаблон має бути відрендерений; фреймворк автоматично визначить шлях, полегшуючи вам кодування. -- `templates//.latte` -- `templates/..latte` +Якщо ви використовуєте структуру каталогів, де кожен презентер має власний каталог, просто розмістіть шаблон у цьому каталозі під назвою дії (тобто подання). Наприклад, для дії `default` використовуйте шаблон `default.latte`: -Якщо шаблон не буде знайдено, він спробує виконати пошук у каталозі `templates` на один рівень вище, тобто на тому ж рівні, що і каталог з класом presenter. +/--pre +app/ +└── Presentation/ + └── Home/ + ├── HomePresenter.php + └── default.latte +\-- -Якщо шаблон не буде знайдено і там, у відповідь буде видано [помилку 404 |presenters#Error 404 etc.]. +Якщо ви використовуєте структуру, де доповідачі знаходяться разом в одному каталозі, а шаблони - в папці `templates`, збережіть її або у файлі `..latte` або `/.latte`: -Ви також можете змінити вигляд за допомогою `$this->setView('jineView')`. Або, замість прямого пошуку, вкажіть ім'я файлу шаблону за допомогою `$this->template->setFile('/path/to/template.latte')`. +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── Home.default.latte ← 1st variant + └── Home/ + └── default.latte ← 2nd variant +\-- + +Директорію `templates` також можна розмістити на один рівень вище, на тому ж рівні, що і директорію з класами ведучого. + +Якщо шаблон не знайдено, доповідач видає [помилку 404 - сторінка не знайдена |presenters#Error 404 etc]. + +Ви можете змінити вигляд за допомогою `$this->setView('anotherView')`. Також можна безпосередньо вказати файл шаблону за допомогою `$this->template->setFile('/path/to/template.latte')`. .[note] -Файли, в яких здійснюється пошук шаблонів, можна змінити, наклавши метод [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], який повертає масив можливих імен файлів. +Файли, в яких шукаються шаблони, можна змінити, перевизначивши метод [formatTemplateFiles( |api:Nette\Application\UI\Presenter::formatTemplateFiles()]), який повертає масив можливих імен файлів. + + +Пошук шаблонів макетів .[#toc-layout-template-lookup] +----------------------------------------------------- + +Nette також автоматично шукає файл макета. + +Якщо ви використовуєте структуру каталогів, де кожен доповідач має власний каталог, розмістіть макет або в папці доповідача, якщо він стосується лише його, або на рівень вище, якщо він є спільним для кількох доповідачів: + +/--pre +app/ +└── Presentation/ + ├── @layout.latte ← common layout + └── Home/ + ├── @layout.latte ← only for Home presenter + ├── HomePresenter.php + └── default.latte +\-- + +Якщо ви використовуєте структуру, в якій ведучі згруповані в одному каталозі, а шаблони знаходяться в папці `templates`, макет буде знаходитися в наступних місцях: -У цих файлах очікується компонування: +/--pre +app/ +└── Presenters/ + ├── HomePresenter.php + └── templates/ + ├── @layout.latte ← common layout + ├── Home.@layout.latte ← only for Home, 1st variant + └── Home/ + └── @layout.latte ← only for Home, 2nd variant +\-- -- `templates//@.latte` -- `templates/.@.latte` -- `templates/@.latte` макет, спільний для кількох доповідачів +Якщо доповідач знаходиться в модулі, він також шукатиме далі по дереву каталогів відповідно до вкладеності модуля. -Де `` це ім'я поточного ведучого і `` це ім'я макета, яке за замовчуванням дорівнює `'layout'`. Ім'я може бути змінено за допомогою `$this->setLayout('jinyLayout')`, тому будуть випробувані файли `@jinyLayout.latte`. +Назву шаблону можна змінити за допомогою `$this->setLayout('layoutAdmin')` і тоді вона буде очікуватися у файлі `@layoutAdmin.latte`. Ви також можете безпосередньо вказати файл шаблону розкладки за допомогою `$this->setLayout('/path/to/template.latte')`. -Ви також можете безпосередньо вказати ім'я файлу шаблону макета за допомогою `$this->setLayout('/path/to/template.latte')`. Використання `$this->setLayout(false)` відключає відстеження макета. +Використання `$this->setLayout(false)` або тегу `{layout none}` всередині шаблону вимикає пошук макетів. .[note] -Файли, в яких здійснюється пошук шаблонів макета, можна змінити, наклавши метод [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], який повертає масив можливих імен файлів. +Файли, в яких шукаються шаблони макетів, можна змінити, перевизначивши метод [formatLayoutTemplateFiles( |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()]), який повертає масив можливих імен файлів. Змінні в шаблоні .[#toc-variables-in-the-template] @@ -104,7 +150,7 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template Ви також можете дозволити собі розкіш шепотіти в шаблонах, просто встановіть плагін Latte в PhpStorm і помістіть ім'я класу на початок шаблону, докладнішу інформацію дивіться в статті "Latte: як набирати систему":https://blog.nette.org/uk/latte-yak-koristuvatisya-sistemoyu-tipiv: ```latte -{templateType App\Presenters\ArticleTemplate} +{templateType App\Presentation\Article\ArticleTemplate} ... ``` @@ -176,7 +222,7 @@ public function beforeRender(): void Latte версії 3 пропонує більш просунутий спосіб створення [розширення |latte:creating-extension] для кожного веб-проекту. Ось короткий приклад такого класу: ```php -namespace App\Templating; +namespace App\Presentation\Accessory; final class LatteExtension extends Latte\Extension { @@ -214,7 +260,7 @@ final class LatteExtension extends Latte\Extension ```neon latte: extensions: - - App\Templating\LatteExtension + - App\Presentation\Accessory\LatteExtension ``` @@ -239,7 +285,7 @@ protected function beforeRender(): void ```neon latte: extensions: - - Latte\Essential\TranslatorExtension + - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` Тоді перекладач можна використовувати, наприклад, як фільтр `|translate`, з додатковими параметрами, переданими методу `translate()` (див. `foo, bar`): diff --git a/best-practices/bg/@home.texy b/best-practices/bg/@home.texy index c3d00d8610..814ac362f9 100644 --- a/best-practices/bg/@home.texy +++ b/best-practices/bg/@home.texy @@ -17,6 +17,8 @@ - [Как да се върнете към предишна страница |restore-request] - [Страница на резултатите от базата данни |Pagination] - [Динамични фрагменти |dynamic-snippets] +- [Как да използвате атрибута #Requires |attribute-requires] +- [Как да използвате правилно POST връзки |post-links]
    @@ -36,10 +38,12 @@ Обща ---- - [Как да заредим конфигурационен файл |bootstrap:] +- [Как да пишем микросайтове |microsites] - [Защо Nette използва константна нотация PascalCase? |https://blog.nette.org/bg/za-po-malko-kresene-v-koda] - [Защо Nette не използва суфикса Interface? |https://blog.nette.org/bg/prefiksite-i-sufiksite-ne-prinadlezat-na-imenata-na-interfejsite] - [Съвети за използване на Composer |composer] - [Съвети за редактори и инструменти |editors-and-tools] +- [Въведение в обектно-ориентираното програмиране |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/bg/attribute-requires.texy b/best-practices/bg/attribute-requires.texy new file mode 100644 index 0000000000..3918ac851d --- /dev/null +++ b/best-practices/bg/attribute-requires.texy @@ -0,0 +1,179 @@ +Как да използвате `#[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 методи .[#toc-http-methods] +-------------------------------- + +Можете да зададете кои 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-method-check]. + + +Извиквания AJAX .[#toc-ajax-calls] +---------------------------------- + +Ако искате даден презентатор или метод да бъде достъпен само за AJAX заявки, използвайте: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +Същият произход .[#toc-same-origin] +----------------------------------- + +За да повишите сигурността, можете да изискате заявката да бъде направена от същия домейн. Това предотвратява [уязвимостта към 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 .[#toc-access-via-forward] +---------------------------------------------- + +Понякога е полезно да се ограничи достъпът до даден презентатор, така че той да е достъпен само косвено, например чрез методите `forward()` или `switch()` от друг презентатор. По този начин се защитават презентаторите за грешки, за да се предотврати задействането им от URL адрес: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\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 + { + } +} +``` + + +Специфични действия .[#toc-specific-actions] +-------------------------------------------- + +Можете също така да ограничите достъпа до определен код, като например създаване на компонент, само за определени действия в презентатора: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +За едно действие не е необходимо да се пише масив: `#[Requires(actions: 'default')]` + + +Потребителски атрибути .[#toc-custom-attributes] +------------------------------------------------ + +Ако искате да използвате `#[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 +{ +} +``` + + +Заключение .[#toc-conclusion] +----------------------------- + +На `#[Requires]` ви дава голяма гъвкавост и контрол върху начина, по който се осъществява достъпът до вашите уеб страници. С помощта на прости, но мощни правила можете да повишите сигурността и правилното функциониране на вашето приложение. Както виждате, използването на атрибути в Nette може не само да опрости работата ви, но и да я осигури. + +{{sitename: Best Practices}} diff --git a/best-practices/bg/composer.texy b/best-practices/bg/composer.texy index b8a26ebb20..2852264f64 100644 --- a/best-practices/bg/composer.texy +++ b/best-practices/bg/composer.texy @@ -142,6 +142,12 @@ composer require php 8.2.3 --no-update ``` +Пренебрегване на версията на PHP .[#toc-ignoring-php-version] +============================================================= + +Пакетите обикновено посочват както най-ниската версия на PHP, с която са съвместими, така и най-високата версия, с която са тествани. Ако планирате да използвате още по-нова версия на PHP, може би за целите на тестването, Composer ще откаже да инсталира такъв пакет. Решението е да използвате опцията `--ignore-platform-req=php+`, която кара Composer да игнорира горните граници на изискваната версия на PHP. + + Фалшиви доклади .[#toc-false-reports] ===================================== @@ -183,7 +189,7 @@ Packagist.org - глобално хранилище .[#toc-packagist-org-global- Впоследствие трябва да стартирате командата `composer dumpautoload` при всяка промяна и да позволите на таблиците на автоматичния модул да се възстановят. Това е изключително неудобно и е много по-добре да оставите тази задача на [RobotLoader |robot-loader:], който върши същата работа автоматично във фонов режим и много по-бързо. -Вторият вариант е да следвате [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Казано по-просто, това е система, в която пространствата от имена и имената на класовете съответстват на структурата на директориите и имената на файловете, т.е. `App\Router\RouterFactory` се намира в `/path/to/App/Router/RouterFactory.php`. Примерна конфигурация: +Вторият вариант е да следвате [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Казано по-просто, това е система, в която пространствата от имена и имената на класовете съответстват на структурата на директориите и имената на файловете, т.е. `App\Core\RouterFactory` се намира в `/path/to/App/Core/RouterFactory.php`. Примерна конфигурация: ```js { diff --git a/best-practices/bg/dynamic-snippets.texy b/best-practices/bg/dynamic-snippets.texy index b927d707e4..7b2db16d21 100644 --- a/best-practices/bg/dynamic-snippets.texy +++ b/best-practices/bg/dynamic-snippets.texy @@ -35,7 +35,7 @@ public function handleUnlike(int $articleId): void Ajaxisation .[#toc-ajaxization] =============================== -Сега нека въведем AJAX в това просто приложение. Промяната на класацията на дадена статия не е достатъчно важна, за да изисква HTTP заявка с пренасочване, така че в идеалния случай това трябва да се прави с AJAX във фонов режим. Ще използваме [скрипта на манипулатора от добавките |https://componette.org/vojtech-dobes/nette.ajax.js/] с обичайната конвенция, че връзките AJAX имат CSS клас `ajax`. +Сега нека въведем AJAX в това просто приложение. Промяната на класацията на дадена статия не е достатъчно важна, за да изисква HTTP заявка с пренасочване, така че в идеалния случай това трябва да се прави с AJAX във фонов режим. Ще използваме [скрипта на манипулатора от добавките |application:ajax#toc-naja] с обичайната конвенция, че връзките AJAX имат CSS клас `ajax`. Как точно да го направим обаче? Nette предлага 2 начина: метода на динамичните фрагменти и метода на компонентите. И двете имат своите плюсове и минуси, затова ще ги покажем последователно. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ class LikeControl extends Nette\Application\UI\Control ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/bg/form-reuse.texy b/best-practices/bg/form-reuse.texy index 9470a63875..6e65e1bc8a 100644 --- a/best-practices/bg/form-reuse.texy +++ b/best-practices/bg/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -Много е важно обвързването между класовете `FormFactory` и `EditFormFactory` да се реализира чрез композиция, а не чрез наследяване на обекти: +Много е важно връзката между класовете `FormFactory` и `EditFormFactory` да се осъществява [чрез композиция |nette:introduction-to-object-oriented-programming#composition], а не [чрез |nette:introduction-to-object-oriented-programming#composition] [наследяване на обекти |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance]: ```php // ⛔ НЕ! НАСЛЕДСТВОТО НЕ ПРИНАДЛЕЖИ ТУК @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory Използването на наследяване в този случай би било напълно непродуктивно. Много бързо ще се сблъскате с проблеми. Например, ако искате да добавите параметри към метода `create()`; PHP ще отчете грешка, че сигнатурата му е различна от тази на родителя. Или при предаване на зависимост на класа `EditFormFactory` чрез конструктора. Това би довело до това, което наричаме " [ад на конструкторите" |dependency-injection:passing-dependencies#Constructor hell]. -Като цяло е по-добре да се предпочита композицията пред наследяването. +Като цяло е по-добре да се предпочита [композицията пред наследяването |dependency-injection:faq#Why composition is preferred over inheritance]. Обработка на формуляри .[#toc-form-handling] @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/bg/lets-create-contact-form.texy b/best-practices/bg/lets-create-contact-form.texy index b0f3d7ccf6..80a29d0e4e 100644 --- a/best-practices/bg/lets-create-contact-form.texy +++ b/best-practices/bg/lets-create-contact-form.texy @@ -39,7 +39,7 @@ class HomePresenter extends Presenter Но какво ще стане, ако потребителят не попълни някои полета? В такъв случай трябва да го уведомим, че това е задължително поле. Направихме това с метода `setRequired()`. Накрая добавихме и [събитие |nette:glossary#events] `onSuccess`, което се задейства, ако формулярът е изпратен успешно. В нашия случай то извиква метода `contactFormSucceeded`, който се грижи за обработката на изпратения формуляр. След малко ще добавим това към кода. -Нека компонентът `contantForm` бъде визуализиран в шаблона `templates/Home/default.latte`: +Нека компонентът `contantForm` бъде визуализиран в шаблона `Home/default.latte`: ```latte {block content} diff --git a/best-practices/bg/microsites.texy b/best-practices/bg/microsites.texy new file mode 100644 index 0000000000..535babfa3f --- /dev/null +++ b/best-practices/bg/microsites.texy @@ -0,0 +1,63 @@ +Как да пишем микросайтове +************************* + +Представете си, че трябва бързо да създадете малък уебсайт за предстоящо събитие на вашата компания. Той трябва да бъде прост, бърз и без излишни усложнения. Може би си мислите, че такъв малък проект не изисква стабилна рамка. Но какво ще кажете, ако използването на рамката Nette може значително да опрости и ускори този процес? + +Дори когато създавате прости уебсайтове, не искате да се отказвате от комфорта. Не искате да изобретявате колелото наново. Чувствайте се свободни да бъдете мързеливи и да се поглезите. Рамката Nette може да се използва отлично и като микрорамка. + +Как може да изглежда един такъв микросайт? Например, целият код на уебсайта може да бъде поставен в един файл `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('', 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#dynamic-routing-with-callbacks] и накрая стартира приложението. Предимството е, че вторият параметър на функцията `addRoute()` може да бъде извикващо се име, което се изпълнява при отваряне на съответната страница. + + +Защо да използвате Nette за микросайтове? .[#toc-why-use-nette-for-microsites] +------------------------------------------------------------------------------ + +- Разработчиците, които някога са опитвали [Трейси |tracy:], днес не могат да си представят кодирането без него. +- Но преди всичко ще използвате системата за шаблониране [Latte |latte:], защото само от 2 страници ще искате да разделите [оформлението и съдържанието |latte:template-inheritance]. +- И със сигурност искате да разчитате на [автоматичното ескапиране, |latte:safety-first] за да предотвратите XSS уязвимости. +- Nette също така гарантира, че в случай на грешка никога няма да бъдат показвани съобщения за грешка на PHP, а вместо това ще бъде показана удобна за потребителя страница. +- Ако искате да получавате обратна връзка от потребителите, например под формата на форма за контакт, можете да добавите и [форми |forms:] и [база данни |database:]. +- Можете също така лесно да [изпратите |mail:] попълнените формуляри [по имейл |mail:]. +- Понякога може да ви е полезно [кеширането |caching:], например при изтегляне и показване на емисии. + +В днешната епоха, в която скоростта и ефективността са от ключово значение, е важно да разполагате с инструменти, които ви позволяват да постигате резултати без излишни забавяния. Рамката Nette предлага точно това - бърза разработка, сигурност и широк набор от инструменти като Tracy и Latte, които опростяват процеса. Достатъчно е да инсталирате няколко пакета на Nette и изграждането на такъв микросайт се превръща в лесна задача. И знаете, че няма скрити недостатъци в сигурността. diff --git a/best-practices/bg/pagination.texy b/best-practices/bg/pagination.texy index be7883dffc..10e88e880f 100644 --- a/best-practices/bg/pagination.texy +++ b/best-practices/bg/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository След това въвеждаме класа на модела в презентатора и в метода `render` правим справка за публикуваните статии, които предаваме на шаблона: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -В шаблона се грижим за извеждането на списъка със статии: +След това шаблонът `default.latte` ще се погрижи за изброяването на статиите: ```latte {block content} @@ -114,7 +113,7 @@ class ArticleRepository Също така разширяваме метода `render`, за да получим инстанцията Paginator, да я конфигурираме и да изберем желаните статии, които да се показват в шаблона. HomePresenter ще изглежда по следния начин: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ class HomePresenter extends Nette\Application\UI\Presenter ``` -Ето как добавихме страниране с помощта на Paginator. Ако използваме [Nette Database Core |database:core] като слой на базата данни вместо [Nette Database Explorer |database:explorer], можем да реализираме странициране дори без Paginator. Класът `Nette\Database\Table\Selection` съдържа метод [page |api:Nette\Database\Table\Selection::_ page] с логика за страниране, взета от Paginator. +Ето как добавихме страниране с помощта на 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. Хранилището ще изглежда по следния начин: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository Не е необходимо да създаваме Paginator в презентатора, вместо това ще използваме метода на обекта `Selection`, върнат от хранилището: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/bg/post-links.texy b/best-practices/bg/post-links.texy new file mode 100644 index 0000000000..f9299c9cbe --- /dev/null +++ b/best-practices/bg/post-links.texy @@ -0,0 +1,59 @@ +Как правилно да използвате POST връзки +************************************** + +В уеб приложенията, особено в административните интерфейси, трябва да е основно правило, че действия, променящи състоянието на сървъра, не трябва да се извършват чрез метода HTTP GET. Както подсказва името на метода, GET трябва да се използва само за извличане на данни, а не за тяхната промяна. +За действия като изтриване на записи е по-подходящо да се използва методът POST. Въпреки че идеалният вариант би бил да се използва методът DELETE, той не може да бъде извикан без JavaScript, поради което исторически се използва POST. + +Как да го направим на практика? Използвайте този прост трик. В началото на вашия шаблон създайте помощна форма с идентификатор `postForm`, която след това ще използвате за бутоните за изтриване: + +```latte .{file:@layout.latte} +
    +``` + +С тази форма можете да използвате `
    @@ -36,10 +38,12 @@ Formuláře Obecné ------ - [Jak načíst konfigurační soubor |bootstrap:] +- [Jak psát mikro-weby |microsites] - [Proč Nette používá PascalCase notaci konstant? |https://blog.nette.org/cs/za-mene-kriku-v-kodu] - [Proč Nette nepoužívá příponu Interface? |https://blog.nette.org/cs/predpony-a-pripony-do-nazvu-rozhrani-nepatri] - [Composer: tipy pro použití |composer] - [Tipy na editory & nástroje |editors-and-tools] +- [Úvod do objektově orientovaného programování |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/cs/attribute-requires.texy b/best-practices/cs/attribute-requires.texy new file mode 100644 index 0000000000..fdde683a91 --- /dev/null +++ b/best-practices/cs/attribute-requires.texy @@ -0,0 +1,179 @@ +Jak používat atribut `#[Requires]` +********************************** + +.[perex] +Když píšete webovou aplikaci, často se setkáte s potřebou omezit přístup k určitým částem vaší aplikace. Možná chcete, aby některé požadavky mohly odesílat data pouze pomocí formuláře (tedy metodou POST), nebo aby byly přístupné pouze pro AJAXové volání. V Nette Frameworku 3.2 se objevil nový nástroj, který vám umožní taková omezení nastavit velmi elegantně a přehledně: atribut `#[Requires]`. + +Atribut je speciální značka v PHP, kterou přidáte před definici třídy nebo metody. Protože jde vlastně o třídu, aby vám následující příklady fungovaly, je nutné uvést klauzuli use: + +```php +use Nette\Application\Attributes\Requires; +``` + +Atribut `#[Requires]` můžete použít u samotné třídy presenteru a také na těchto metodách: + +- `action()` +- `render()` +- `handle()` +- `createComponent()` + +Poslední dvě metody se týkají i komponent, tedy atribut můžete používat i u nich. + +Pokud nejsou splněny podmínky, které atribut uvádí, dojde k vyvolání HTTP chyby 4xx. + + +Metody HTTP +----------- + +Můžete specifikovat, které HTTP metody (jako GET, POST atd.) jsou pro přístup povolené. Například, pokud chcete povolit přístup pouze odesíláním formuláře, nastavíte: + +```php +class AdminPresenter extends Nette\Application\UI\Presenter +{ + #[Requires(methods: 'POST')] + public function actionDelete(int $id): void + { + } +} +``` + +Proč byste měli používat POST místo GET pro akce měnící stav a jak na to? [Přečtěte si návod |post-links]. + +Můžete uvést metodu nebo pole metod. Speciálním případem je hodnota `'*'`, která povolí všechny metody, což standardně presentery z [bezpečnostních důvodů nedovolují |application:presenters#toc-kontrola-http-metody]. + + +AJAXové volání +-------------- + +Pokud chcete, aby byl presenter nebo metoda dostupná pouze pro AJAXové požadavky, použijte: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +Stejný původ +------------ + +Pro zvýšení bezpečnosti můžete vyžadovat, aby byl požadavek učiněn ze stejné domény. Tím zabráníte [zranitelnosti CSRF |nette:vulnerability-protection#cross-site-request-forgery-csrf]: + +```php +#[Requires(sameOrigin: true)] +class SecurePresenter extends Nette\Application\UI\Presenter +{ +} +``` + +U metod `handle()` je přístup ze stejné domény vyžadován automaticky. Takže pokud naopak chcete povolit přístup z jakékoliv domény, uveďte: + +```php +#[Requires(sameOrigin: false)] +public function handleList(): void +{ +} +``` + + +Přístup přes forward +-------------------- + +Někdy je užitečné omezit přístup k presenteru tak, aby byl dostupný pouze nepřímo, například použitím metody `forward()` nebo `switch()` z jiného presenteru. Takto se třeba chrání error-presentery, aby je nebylo možné vyvolat z URL: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +V praxi bývá často potřeba označit určité views, ke kterým se lze dostat až na základě logiky v presenteru. Tedy opět, aby je nebylo možné otevřít přímo: + +```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étní akce +-------------- + +Můžete také omezit, že určitý kód, třeba vytvoření komponenty, bude dostupné pouze pro specifické akce v presenteru: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +V případě jedné akce není potřeba zapisovat pole: `#[Requires(actions: 'default')]` + + +Vlastní atributy +---------------- + +Pokud chcete použít atribut `#[Requires]` opakovaně s týmž nastavením, můžete si vytvořit vlastní atribut, který bude dědit `#[Requires]` a nastaví ho podle potřeb. + +Například `#[SingleAction]` umožní přístup pouze přes akci `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 +{ +} +``` + +Nebo `#[RestMethods]` umožní přístup přes všechny HTTP metody používané pro 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 +{ +} +``` + + +Závěr +----- + +Atribut `#[Requires]` vám dává velkou flexibilitu a kontrolu nad tím, jak jsou vaše webové stránky přístupné. Pomocí jednoduchých, ale mocných pravidel můžete zvýšit bezpečnost a správné fungování vaší aplikace. Jak vidíte, použití atributů v Nette může vaši práci nejen usnadnit, ale i zabezpečit. + +{{sitename: Best Practices}} diff --git a/best-practices/cs/composer.texy b/best-practices/cs/composer.texy index 2f0c503870..137b09c745 100644 --- a/best-practices/cs/composer.texy +++ b/best-practices/cs/composer.texy @@ -58,7 +58,7 @@ composer update Composer stáhne Nette Database do složky `vendor/`. Dále vytvoří soubor `composer.lock`, který obsahuje informace o tom, které verze knihoven přesně nainstaloval. -Composer vygeneruje soubor `vendor/autoload.php`, který můžeme jednoduše zainkludovat a začít používat knihovny bez jakékoli další práce: +Composer vygeneruje soubor `vendor/autoload.php`, který můžeme jednoduše inkludovat a začít používat knihovny bez jakékoli další práce: ```php require __DIR__ . '/vendor/autoload.php'; @@ -142,6 +142,12 @@ Nebo přímo v souboru `composer.json`: ``` +Ignorování verze PHP +==================== + +Balíčky zpravidla mívají uvedenou jak nejnižší verzi PHP, se kterou jsou kompatibilní, tak i nejvyšší, se kterou jsou testované. Pokud se chystáte používat verzi PHP ještě novější, třeba z důvodu testování, Composer odmítne takový balíček nainstalovat. Řešením je volba `--ignore-platform-req=php+`, která způsobí, že Composer bude ignorovat horní limity požadované verze PHP. + + Planá hlášení ============= @@ -183,7 +189,7 @@ Nicméně je možné používat Composer i pro načítání dalších tříd i m Následně je potřeba při každé změně spustit příkaz `composer dumpautoload` a nechat autoloadovací tabulky přegenerovat. To je nesmírně nepohodlné a daleko lepší je tento úkol svěřit [RobotLoaderu|robot-loader:], který stejnou činnost provádí automaticky na pozadí a mnohem rychleji. -Druhou možností je dodržovat [PSR-4|https://www.php-fig.org/psr/psr-4/]. Zjednodušeně řečeno jde o systém, kdy jmenné prostory a názvy tříd odpovídají adresářové struktuře a názvům souborů, tedy např. `App\Router\RouterFactory` bude v souboru `/path/to/App/Router/RouterFactory.php`. Příklad konfigurace: +Druhou možností je dodržovat [PSR-4|https://www.php-fig.org/psr/psr-4/]. Zjednodušeně řečeno jde o systém, kdy jmenné prostory a názvy tříd odpovídají adresářové struktuře a názvům souborů, tedy např. `App\Core\RouterFactory` bude v souboru `/path/to/App/Core/RouterFactory.php`. Příklad konfigurace: ```js { diff --git a/best-practices/cs/dynamic-snippets.texy b/best-practices/cs/dynamic-snippets.texy index 600dc4b9bd..daf3b00928 100644 --- a/best-practices/cs/dynamic-snippets.texy +++ b/best-practices/cs/dynamic-snippets.texy @@ -35,7 +35,7 @@ public function handleUnlike(int $articleId): void Ajaxizace ========= -Pojďme nyní tuto jednoduchou aplikaci vybavit AJAXem. Změna hodnocení článku není natolik důležitá, aby muselo dojít k přesměrování, a proto by ideálně měla probíhat AJAXem na pozadí. Využijeme [obslužného skriptu z doplňků |https://componette.org/vojtech-dobes/nette.ajax.js/] s obvyklou konvencí, že AJAXové odkazy mají CSS třídu `ajax`. +Pojďme nyní tuto jednoduchou aplikaci vybavit AJAXem. Změna hodnocení článku není natolik důležitá, aby muselo dojít k přesměrování, a proto by ideálně měla probíhat AJAXem na pozadí. Využijeme [obslužného skriptu z doplňků |application:ajax#toc-naja] s obvyklou konvencí, že AJAXové odkazy mají CSS třídu `ajax`. Nicméně jak na to konkrétně? Nette nabízí 2 cesty: cestu tzv. dynamických snippetů a cestu komponent. Obě dvě mají svá pro a proti, a proto si je ukážeme jednu po druhé. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ Samozřejmě se nám změní šablona view a do presenteru budeme muset doplnit ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/cs/form-reuse.texy b/best-practices/cs/form-reuse.texy index c659b8bcb4..2a6db194db 100644 --- a/best-practices/cs/form-reuse.texy +++ b/best-practices/cs/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -Velmi důležité je, aby vazba mezi třídami `FormFactory` a `EditFormFactory` byla realizována kompozicí, nikoliv objektovou dědičností: +Velmi důležité je, aby vazba mezi třídami `FormFactory` a `EditFormFactory` byla realizována [kompozicí|nette:introduction-to-object-oriented-programming#kompozice], nikoliv [objektovou dědičností|https://doc.nette.org/cs/introduction-to-object-oriented-programming#dedicnost]: ```php // ⛔ TAKHLE NE! SEM DĚDIČNOST NEPATŘÍ @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory Použití dedičnosti by bylo v tomto případě zcela kontraproduktivní. Na problémy byste narazili velmi rychle. Třeba ve chvíli, kdybyste chtěli přidat metodě `create()` parametry; PHP by zahlásilo chybu, že se její signatura liší od rodičovské. Nebo při předávání závislosti do třídy `EditFormFactory` přes konstruktor. Nastala by situace, které říkáme [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. -Obecně je lepší dávat přednost kompozici před dědičností. +Obecně je lepší dávat přednost [kompozici před dědičností|dependency-injection:faq#Proč se upřednostňuje kompozice před dědičností]. Obsluha formuláře @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/cs/inject-method-attribute.texy b/best-practices/cs/inject-method-attribute.texy index 9bd667a09f..b3e80f1266 100644 --- a/best-practices/cs/inject-method-attribute.texy +++ b/best-practices/cs/inject-method-attribute.texy @@ -61,7 +61,7 @@ class MyPresenter extends Nette\Application\UI\Presenter Výhodou tohoto způsobu předávání závislostí byla velice úsporná podoba zápisu. Nicméně s příchodem [constructor property promotion |https://blog.nette.org/cs/php-8-0-kompletni-prehled-novinek#toc-constructor-property-promotion] se jeví snazší použít konstruktor. -Naopak tento způsob trpí stejnými nedostatky, jako předávání závislosti do properites obecně: nemáme kontrolu nad změnami v proměnné a zároveň se proměnná stává součástí veřejného rozhraní třídy, což je nežádnoucí. +Naopak tento způsob trpí stejnými nedostatky, jako předávání závislosti do properties obecně: nemáme kontrolu nad změnami v proměnné a zároveň se proměnná stává součástí veřejného rozhraní třídy, což je nežádnoucí. {{sitename: Best Practices}} diff --git a/best-practices/cs/lets-create-contact-form.texy b/best-practices/cs/lets-create-contact-form.texy index 78f2f92886..aa24fb819f 100644 --- a/best-practices/cs/lets-create-contact-form.texy +++ b/best-practices/cs/lets-create-contact-form.texy @@ -39,7 +39,7 @@ Jak vidíte, vytvořili jsme dvě metody. První metoda `createComponentContactF Ale co když uživatel nevyplní nějaké pole? V takovém případě bychom mu měli dát vědět, že je to povinné pole. Toho jsme docílili metodou `setRequired()`. Nakonec jsme přidali také [událost |nette:glossary#Události] `onSuccess`, která se spustí, pokud je formulář úspěšně odeslán. V našem případě zavolá metodu `contactFormSucceeded`, která se postará o zpracování odeslaného formuláře. To do kódu doplníme za okamžik. -Komponentu `contantForm` necháme vykreslit v šabloně `templates/Home/default.latte`: +Komponentu `contantForm` necháme vykreslit v šabloně `Home/default.latte`: ```latte {block content} @@ -131,7 +131,7 @@ Zatím se odesílá prostý textový email obsahující pouze zprávu odeslanou ``` -Zbývá upravit `ContactFacade`, aby tuto šablonu používal. V konstruktoru si vyžádáme třídu `LatteFactory`, která umí vyrobit objekt `Latte\Engine`, tedy [vykreslovač Latte šablon |latte:develop#jak-vykreslit-sablonu]. Pomocí metody `renderToString()` šablonu vykreslíme do souboru, prvním parametrem je cesta k šabloně a druhým jsou proměnné. +Zbývá upravit `ContactFacade`, aby tuto šablonu používal. V konstruktoru si vyžádáme třídu `LatteFactory`, která umí vyrobit objekt `Latte\Engine`, tedy [vykreslovač Latte šablon |latte:develop#vykresleni-sablony]. Pomocí metody `renderToString()` šablonu vykreslíme do souboru, prvním parametrem je cesta k šabloně a druhým jsou proměnné. ```php namespace App\Model; diff --git a/best-practices/cs/microsites.texy b/best-practices/cs/microsites.texy new file mode 100644 index 0000000000..17a9fa7e41 --- /dev/null +++ b/best-practices/cs/microsites.texy @@ -0,0 +1,63 @@ +Jak psát mikro-weby +******************* + +Představte si, že potřebujete rychle vytvořit malý web pro nadcházející akci vaší firmy. Má to být jednoduché, rychlé a bez zbytečných komplikací. Možná si myslíte, že pro tak malý projekt nepotřebujete robustní framework. Ale co když použití Nette frameworku může tento proces zásadně zjednodušit a zrychlit? + +Přece i při tvorbě jednoduchých webů se nechcete vzdát pohodlí. Nechcete vymýšlet to, co už bylo jednou vyřešené. Buďte klidně líný a nechte se rozmazlovat. Nette Framework lze skvěle využít i jako micro framework. + +Jak takový microsite může vypadat? Například tak, že celý kód webu umístíme do jediného souboru `index.php` ve veřejné složce: + +```php +enableTracy(__DIR__ . '/../log'); +$configurator->setTempDirectory(__DIR__ . '/../temp'); + +// vytvoř DI kontejner na základě konfigurace v config.neon +$configurator->addConfig(__DIR__ . '/../app/config.neon'); +$container = $configurator->createContainer(); + +// nastavíme routing +$router = new Nette\Application\Routers\RouteList; +$container->addService('router', $router); + +// routa pro URL https://example.com/ +$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { + // detekujeme jazyk prohlížeče a přesměrujeme na URL /en nebo /de atd. + $supportedLangs = ['en', 'de', 'cs']; + $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); + $presenter->redirectUrl("/$lang"); +}); + +// routa pro URL https://example.com/cs nebo https://example.com/en +$router->addRoute('', function ($presenter, string $lang) { + // zobrazíme příslušnou šablonu, například ../templates/en.latte + $template = $presenter->createTemplate() + ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); + return $template; +}); + +// spusť aplikaci! +$container->getByType(Nette\Application\Application::class)->run(); +``` + +Vše ostatní budou šablony uložené v nadřazené složce `/templates`. + +PHP kód v `index.php` nejprve [připraví prostředí |bootstrap:], poté definuje [routy|application:routing#dynamicke-routovani-s-callbacky] a nakonec spustí aplikaci. Výhodou je, že druhý parametr funkce `addRoute()` může být callable, který se po otevření odpovídající stránky vykoná. + + +Proč používat Nette pro microsite? +---------------------------------- + +- Programátoři, kteří někdy vyzkoušeli [Tracy|tracy:], si dnes neumí představit, že by něco programovali bez ní. +- Především ale využijete šablonovací systém [Latte|latte:], protože už od 2 stránek budete chtít mít oddělený [layout a obsah|latte:template-inheritance]. +- A rozhodně se chcete spolehout na [automatické escapování |latte:safety-first], aby nevznikla zranitelnost XSS +- Nette taky zajistí, že se při chybě nikdy neobrazí programátorské chybové hlášky PHP, ale uživateli srozumitelná stránka. +- Pokud chcete získávat zpětnou vazbu od uživatelů, například v podobě kontaktního formuláře, tak ještě přidáte [formuláře|forms:] a [databázi|database:]. +- Vyplněné formuláře si taktéž můžete nechat snadno [odesílat emailem|mail:]. +- Někdy se vám může hodit [kešování|caching:], například pokud stahujete a zobrazujete feedy. + +V dnešní době, kdy je rychlost a efektivita klíčová, je důležité mít nástroje, které vám umožní dosáhnout výsledků bez zbytečného zdržování. Nette framework vám nabízí právě to - rychlý vývoj, bezpečnost a širokou škálu nástrojů, jako je Tracy a Latte, které zjednodušují proces. Stačí nainstalovat pár Nette balíčků a vybudovat takovou microsite je najednou úplná hračka. A víte, že se nikde neskrývá žádná bezpečnostní díra. diff --git a/best-practices/cs/pagination.texy b/best-practices/cs/pagination.texy index 46a00cd256..c1174c4c48 100644 --- a/best-practices/cs/pagination.texy +++ b/best-practices/cs/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository V presenteru si pak injectujeme modelovou třídu a v render metodě si vyžádáme publikované články, které předáme do šablony: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -V šabloně se pak postaráme o výpis článků: +V šabloně `default.latte` se pak postaráme o výpis článků: ```latte {block content} @@ -114,7 +113,7 @@ Následně se pustíme do úprav presenteru. Do render metody budeme předávat Dále také render metodu rozšíříme o získání instance Paginatoru, jeho nastavení a výběru správných článků pro zobrazení v šabloně. HomePresenter bude po úpravách vypadat takto: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ class HomePresenter extends Nette\Application\UI\Presenter ``` -Takto jsme doplnili stránku o možnost stránkování pomocí Paginatoru. V případě, kdy namísto [Nette Database Core |database:core] jako databázovou vrstvu použijeme [Nette Database Explorer |database:explorer], jsme schopni implementovat stránkování i bez použití Paginatoru. Třída `Nette\Database\Table\Selection` totiž obsahuje metodu [page |api:Nette\Database\Table\Selection::_page] s logikou stránkování převzatou z Paginatoru. +Takto jsme doplnili stránku o možnost stránkování pomocí Paginatoru. V případě, kdy namísto [Nette Database Core |database:sql-way] jako databázovou vrstvu použijeme [Nette Database Explorer |database:explorer], jsme schopni implementovat stránkování i bez použití Paginatoru. Třída `Nette\Database\Table\Selection` totiž obsahuje metodu [page |api:Nette\Database\Table\Selection::_page] s logikou stránkování převzatou z Paginatoru. Repozitář bude při tomto způsobu implementace vypadat takto: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository V presenteru nemusíme vytvářet Paginator, použijeme místo něj metodu třídy `Selection`, kterou nám vrací repositář: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/cs/post-links.texy b/best-practices/cs/post-links.texy new file mode 100644 index 0000000000..e82bb4798d --- /dev/null +++ b/best-practices/cs/post-links.texy @@ -0,0 +1,59 @@ +Jak správně používat POST odkazy +******************************** + +Ve webových aplikacích, zejména v administrativních rozhraních, by mělo být základním pravidlem, že akce měnící stav serveru by neměly být prováděny prostřednictvím HTTP metody GET. Jak název metody napovídá, GET by měl sloužit pouze k získání dat, nikoli k jejich změně. +Pro akce jako třeba mazání záznamů je vhodnější použít metodu POST. I když ideální by byla metoda DELETE, ale tu nelze bez JavaScriptu vyvolat, proto se historicky používá POST. + +Jak na to v praxi? Využijte tento jednoduchý trik. Na začátku šablony si vytvoříte pomocný formulář s identifikátorem `postForm`, který následně použijete pro mazací tlačítka: + +```latte .{file:@layout.latte} +
    +``` + +Díky tomuto formuláři můžete místo klasického odkazu `
    ` použít tlačítko `
    @@ -36,10 +38,12 @@ Formulare Allgemein --------- - [Wie man eine Konfigurationsdatei lädt |bootstrap:] +- [Wie man Microsites schreibt |microsites] - [Warum verwendet Nette die Konstantenschreibweise PascalCase? |https://blog.nette.org/de/fuer-weniger-geschrei-im-code] - [Warum verwendet Nette nicht das Suffix Interface? |https://blog.nette.org/de/praefixe-und-suffixe-gehoeren-nicht-in-schnittstellennamen] - [Tipps zur Verwendung des Composers |composer] - [Tipps zu Editoren und Tools |editors-and-tools] +- [Einführung in die objektorientierte Programmierung |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/de/attribute-requires.texy b/best-practices/de/attribute-requires.texy new file mode 100644 index 0000000000..853a90b4f5 --- /dev/null +++ b/best-practices/de/attribute-requires.texy @@ -0,0 +1,179 @@ +Wie man das `#[Requires]` Attribut +********************************** + +.[perex] +Wenn Sie eine Webanwendung schreiben, stoßen Sie häufig auf die Notwendigkeit, den Zugriff auf bestimmte Teile Ihrer Anwendung zu beschränken. Vielleicht möchten Sie, dass einige Anfragen nur Daten über ein Formular senden können (also die POST-Methode verwenden) oder nur für AJAX-Aufrufe zugänglich sind. In Nette Framework 3.2 wurde ein neues Werkzeug eingeführt, mit dem Sie solche Einschränkungen elegant und klar festlegen können: das `#[Requires]` Attribut. + +Das Attribut ist eine spezielle Markierung in PHP, die Sie vor der Definition einer Klasse oder Methode hinzufügen. Da es sich im Wesentlichen um eine Klasse handelt, müssen Sie die Use-Klausel einfügen, damit die folgenden Beispiele funktionieren: + +```php +use Nette\Application\Attributes\Requires; +``` + +Sie können das `#[Requires]` Attribut mit der Presenter-Klasse selbst und mit diesen Methoden verwenden: + +- `action()` +- `render()` +- `handle()` +- `createComponent()` + +Die letzten beiden Methoden betreffen auch Komponenten, so dass Sie das Attribut auch für diese verwenden können. + +Wenn die durch das Attribut festgelegten Bedingungen nicht erfüllt sind, wird ein HTTP 4xx-Fehler ausgelöst. + + +HTTP-Methoden .[#toc-http-methods] +---------------------------------- + +Sie können angeben, welche HTTP-Methoden (wie GET, POST usw.) für den Zugriff zugelassen sind. Wenn Sie beispielsweise den Zugriff nur durch das Absenden eines Formulars erlauben wollen, legen Sie fest: + +```php +class AdminPresenter extends Nette\Application\UI\Presenter +{ + #[Requires(methods: 'POST')] + public function actionDelete(int $id): void + { + } +} +``` + +Warum sollten Sie POST anstelle von GET für zustandsändernde Aktionen verwenden, und wie geht das? [Lesen Sie den Leitfaden |post-links]. + +Sie können eine Methode oder eine Reihe von Methoden angeben. Ein Sonderfall ist der Wert `'*'`, um alle Methoden zu aktivieren, was Presenter aus [Sicherheitsgründen |application:presenters#http-method-check] standardmäßig nicht zulassen. + + +AJAX-Aufrufe .[#toc-ajax-calls] +------------------------------- + +Wenn Sie möchten, dass ein Presenter oder eine Methode nur für AJAX-Anfragen zugänglich ist, verwenden Sie: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +Gleiche Herkunft .[#toc-same-origin] +------------------------------------ + +Um die Sicherheit zu erhöhen, können Sie verlangen, dass die Anfrage von der gleichen Domäne aus gestellt wird. Dies verhindert eine [Anfälligkeit für CSRF |nette:vulnerability-protection#cross-site-request-forgery-csrf]: + +```php +#[Requires(sameOrigin: true)] +class SecurePresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Für `handle()` Methoden ist der Zugriff aus derselben Domäne automatisch erforderlich. Wenn Sie also den Zugriff aus einer beliebigen Domäne zulassen wollen, geben Sie dies an: + +```php +#[Requires(sameOrigin: false)] +public function handleList(): void +{ +} +``` + + +Zugang über Forward .[#toc-access-via-forward] +---------------------------------------------- + +Manchmal ist es sinnvoll, den Zugriff auf einen Präsentator so einzuschränken, dass er nur indirekt verfügbar ist, z. B. über die Methoden `forward()` oder `switch()` eines anderen Präsentators. Auf diese Weise werden Fehlerpräsenter geschützt, um zu verhindern, dass sie von einer URL ausgelöst werden: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +In der Praxis ist es oft notwendig, bestimmte Ansichten zu markieren, auf die nur aufgrund der Logik im Präsentator zugegriffen werden kann. Auch hier gilt, dass sie nicht direkt geöffnet werden können: + +```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 + { + } +} +``` + + +Spezifische Aktionen .[#toc-specific-actions] +--------------------------------------------- + +Sie können auch einschränken, dass bestimmter Code, wie das Erstellen einer Komponente, nur für bestimmte Aktionen im Präsentator zugänglich ist: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +Für eine einzelne Aktion muss kein Array geschrieben werden: `#[Requires(actions: 'default')]` + + +Benutzerdefinierte Attribute .[#toc-custom-attributes] +------------------------------------------------------ + +Wenn Sie das Attribut `#[Requires]` Attribut wiederholt mit denselben Einstellungen verwenden möchten, können Sie ein eigenes Attribut erstellen, das die `#[Requires]` erbt, und es nach Ihren Bedürfnissen einstellen. + +Zum Beispiel, `#[SingleAction]` erlaubt den Zugriff nur über die Aktion `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 +{ +} +``` + +Oder `#[RestMethods]` ermöglicht den Zugriff über alle für die REST-API verwendeten HTTP-Methoden: + +```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 +{ +} +``` + + +Schlussfolgerung .[#toc-conclusion] +----------------------------------- + +Das `#[Requires]` Attribut gibt Ihnen große Flexibilität und Kontrolle darüber, wie auf Ihre Webseiten zugegriffen wird. Mit einfachen, aber leistungsfähigen Regeln können Sie die Sicherheit und das ordnungsgemäße Funktionieren Ihrer Anwendung verbessern. Wie Sie sehen, kann die Verwendung von Attributen in Nette Ihre Arbeit nicht nur vereinfachen, sondern auch sichern. + +{{sitename: Best Practices}} diff --git a/best-practices/de/composer.texy b/best-practices/de/composer.texy index eaaa86a842..10aed8c202 100644 --- a/best-practices/de/composer.texy +++ b/best-practices/de/composer.texy @@ -142,6 +142,12 @@ Oder direkt in der Datei "Composer.json": ``` +PHP-Version ignorieren .[#toc-ignoring-php-version] +=================================================== + +Pakete geben normalerweise sowohl die niedrigste Version von PHP an, mit der sie kompatibel sind, als auch die höchste Version, mit der sie getestet wurden. Wenn Sie vorhaben, eine noch neuere Version von PHP zu verwenden, etwa zu Testzwecken, wird Composer die Installation eines solchen Pakets verweigern. Die Lösung besteht darin, die Option `--ignore-platform-req=php+` zu verwenden, die Composer veranlasst, die Obergrenzen der erforderlichen PHP-Version zu ignorieren. + + False Berichte .[#toc-false-reports] ==================================== @@ -183,7 +189,7 @@ Es ist jedoch auch möglich, Composer zu verwenden, um andere Klassen außerhalb Anschließend müssen Sie bei jeder Änderung den Befehl `composer dumpautoload` ausführen und die Autoloader-Tabellen neu generieren lassen. Dies ist äußerst lästig, und es ist weitaus besser, diese Aufgabe [RobotLoader |robot-loader:] anzuvertrauen, der dieselbe Tätigkeit automatisch im Hintergrund und viel schneller durchführt. -Die zweite Möglichkeit ist, [PSR-4 |https://www.php-fig.org/psr/psr-4/] zu folgen. Einfach gesagt handelt es sich um ein System, bei dem die Namensräume und Klassennamen der Verzeichnisstruktur und den Dateinamen entsprechen, d. h. `App\Router\RouterFactory` befindet sich in der Datei `/path/to/App/Router/RouterFactory.php`. Beispiel für eine Konfiguration: +Die zweite Möglichkeit ist, [PSR-4 |https://www.php-fig.org/psr/psr-4/] zu folgen. Einfach gesagt handelt es sich um ein System, bei dem die Namensräume und Klassennamen der Verzeichnisstruktur und den Dateinamen entsprechen, d. h. `App\Core\RouterFactory` befindet sich in der Datei `/path/to/App/Core/RouterFactory.php`. Beispiel für eine Konfiguration: ```js { diff --git a/best-practices/de/dynamic-snippets.texy b/best-practices/de/dynamic-snippets.texy index c75affd1e4..e4ac99a50a 100644 --- a/best-practices/de/dynamic-snippets.texy +++ b/best-practices/de/dynamic-snippets.texy @@ -35,7 +35,7 @@ Vorlage: Ajaxisierung .[#toc-ajaxization] ================================ -Bringen wir nun AJAX in diese einfache Anwendung. Das Ändern der Bewertung eines Artikels ist nicht wichtig genug, um eine HTTP-Anfrage mit Redirect zu erfordern, also sollte es idealerweise mit AJAX im Hintergrund geschehen. Wir werden das [Handler-Skript aus add-ons |https://componette.org/vojtech-dobes/nette.ajax.js/] verwenden, mit der üblichen Konvention, dass AJAX-Links die CSS-Klasse `ajax` haben. +Bringen wir nun AJAX in diese einfache Anwendung. Das Ändern der Bewertung eines Artikels ist nicht wichtig genug, um eine HTTP-Anfrage mit Redirect zu erfordern, also sollte es idealerweise mit AJAX im Hintergrund geschehen. Wir werden das [Handler-Skript aus add-ons |application:ajax#toc-naja] verwenden, mit der üblichen Konvention, dass AJAX-Links die CSS-Klasse `ajax` haben. Aber wie macht man das konkret? Nette bietet 2 Wege an: den dynamischen Snippet-Weg und den Komponenten-Weg. Beide haben ihre Vor- und Nachteile, daher werden wir sie nacheinander vorstellen. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ Natürlich werden wir die Ansichtsvorlage ändern und dem Präsentator eine Fabr ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/de/form-reuse.texy b/best-practices/de/form-reuse.texy index 08cc17d64f..a68bf02309 100644 --- a/best-practices/de/form-reuse.texy +++ b/best-practices/de/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -Es ist sehr wichtig, dass die Bindung zwischen den Klassen `FormFactory` und `EditFormFactory` durch Komposition und nicht durch Objektvererbung implementiert wird: +Es ist sehr wichtig, dass die Bindung zwischen den Klassen `FormFactory` und `EditFormFactory` [durch Komposition |nette:introduction-to-object-oriented-programming#composition] und nicht [durch |nette:introduction-to-object-oriented-programming#composition] [Objektvererbung |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance] implementiert wird: ```php // ⛔ NEIN! VERERBUNG GEHÖRT HIER NICHT HIN @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory Die Verwendung von Vererbung wäre in diesem Fall völlig kontraproduktiv. Sie würden sehr schnell auf Probleme stoßen. Wenn Sie z.B. der Methode `create()` Parameter hinzufügen wollten, würde PHP einen Fehler melden, dass sich die Signatur der Methode von der des Elternteils unterscheidet. Oder bei der Übergabe einer Abhängigkeit an die Klasse `EditFormFactory` über den Konstruktor. Dies würde zu dem führen, was wir [Konstruktorhölle |dependency-injection:passing-dependencies#Constructor hell] nennen. -Im Allgemeinen ist es besser, Komposition der Vererbung vorzuziehen. +Im Allgemeinen ist es besser, die [Komposition der Vererbung |dependency-injection:faq#Why composition is preferred over inheritance] vorzuziehen. Handhabung von Formularen .[#toc-form-handling] @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/de/lets-create-contact-form.texy b/best-practices/de/lets-create-contact-form.texy index 67b26710b2..886b452767 100644 --- a/best-practices/de/lets-create-contact-form.texy +++ b/best-practices/de/lets-create-contact-form.texy @@ -39,7 +39,7 @@ Wie Sie sehen können, haben wir zwei Methoden erstellt. Die erste Methode `crea Was aber, wenn der Benutzer einige Felder nicht ausfüllt? In diesem Fall sollten wir ihn darauf hinweisen, dass es sich um ein Pflichtfeld handelt. Wir haben dies mit der Methode `setRequired()` getan. Schließlich haben wir auch ein [Ereignis |nette:glossary#events] `onSuccess` hinzugefügt, das ausgelöst wird, wenn das Formular erfolgreich abgeschickt wurde. In unserem Fall ruft es die Methode `contactFormSucceeded` auf, die sich um die Verarbeitung des übermittelten Formulars kümmert. Das fügen wir dem Code gleich hinzu. -Die Komponente `contantForm` soll in der Vorlage `templates/Home/default.latte` gerendert werden: +Die Komponente `contantForm` soll in der Vorlage `Home/default.latte` gerendert werden: ```latte {block content} diff --git a/best-practices/de/microsites.texy b/best-practices/de/microsites.texy new file mode 100644 index 0000000000..f28c89828c --- /dev/null +++ b/best-practices/de/microsites.texy @@ -0,0 +1,63 @@ +Wie man Microsites schreibt +*************************** + +Stellen Sie sich vor, Sie müssen schnell eine kleine Website für eine bevorstehende Veranstaltung Ihres Unternehmens erstellen. Sie soll einfach, schnell und ohne unnötige Komplikationen sein. Sie denken vielleicht, dass ein solch kleines Projekt kein robustes Framework benötigt. Aber was wäre, wenn die Verwendung des Nette-Frameworks diesen Prozess erheblich vereinfachen und beschleunigen könnte? + +Auch bei der Erstellung einfacher Websites wollen Sie nicht auf Komfort verzichten. Sie wollen das Rad nicht neu erfinden. Seien Sie ruhig faul und gönnen Sie sich etwas. Das Nette Framework lässt sich auch hervorragend als Micro-Framework einsetzen. + +Wie könnte eine solche Microsite aussehen? Zum Beispiel kann der gesamte Code der Website in einer einzigen `index.php` Datei im öffentlichen Ordner untergebracht werden: + +```php +<?php + +require __DIR__ . '/../vendor/autoload.php'; + +$configurator = new Nette\Bootstrap\Configurator; +$configurator->enableTracy(__DIR__ . '/../log'); +$configurator->setTempDirectory(__DIR__ . '/../temp'); + +// einen DI-Container auf der Grundlage der Konfiguration in config.neon erstellen +$configurator->addConfig(__DIR__ . '/../app/config.neon'); +$container = $configurator->createContainer(); + +// Routing einrichten +$router = new Nette\Application\Routers\RouteList; +$container->addService('router', $router); + +// Route für URL https://example.com/ +$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { + // Erkennung der Browsersprache und Umleitung auf URL /en oder /de usw. + $supportedLangs = ['en', 'de', 'cs']; + $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); + $presenter->redirectUrl("/$lang"); +}); + +// Route für URL https://example.com/cs nebo https://example.com/en +$router->addRoute('', function ($presenter, string $lang) { + // Anzeige der entsprechenden Vorlage, zum Beispiel ../templates/de.latte + $template = $presenter->createTemplate() + ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); + return $template; +}); + +// Starten Sie die Anwendung! +$container->getByType(Nette\Application\Application::class)->run(); +``` + +Alles andere sind Vorlagen, die im übergeordneten Ordner `/templates` gespeichert werden. + +Der PHP-Code in `index.php` richtet zunächst [die Umgebung |bootstrap:] ein, definiert dann die [Routen |application:routing#dynamic-routing-with-callbacks] und führt schließlich die Anwendung aus. Der Vorteil ist, dass der zweite Parameter der Funktion `addRoute()` eine Callable sein kann, die ausgeführt wird, wenn die entsprechende Seite geöffnet wird. + + +Warum Nette für Microsites verwenden? .[#toc-why-use-nette-for-microsites] +-------------------------------------------------------------------------- + +- Entwickler, die [Tracy |tracy:] einmal ausprobiert haben, können sich heute nicht mehr vorstellen, ohne es zu programmieren. +- Aber vor allem werden Sie das Templating-System [Latte |latte:] nutzen, denn schon ab 2 Seiten werden Sie [Layout und Inhalt |latte:template-inheritance] trennen wollen. +- Und Sie wollen sich auf jeden Fall auf das [automatische Escaping |latte:safety-first] verlassen, um XSS-Schwachstellen zu vermeiden. +- Nette sorgt auch dafür, dass im Falle eines Fehlers niemals PHP-Fehlermeldungen angezeigt werden, sondern eine benutzerfreundliche Seite. +- Wenn Sie Feedback von den Nutzern erhalten möchten, zum Beispiel in Form eines Kontaktformulars, können Sie auch [Formulare |forms:] und eine [Datenbank |database:] hinzufügen. +- Sie können ausgefüllte Formulare auch einfach [per E-Mail versch |mail:]icken lassen. +- In manchen Fällen ist die [Zwischenspeicherung |caching:] nützlich, z. B. beim Herunterladen und Anzeigen von Feeds. + +In der heutigen Zeit, in der Geschwindigkeit und Effizienz entscheidend sind, ist es wichtig, über Werkzeuge zu verfügen, die es Ihnen ermöglichen, Ergebnisse ohne unnötige Verzögerungen zu erzielen. Das Nette-Framework bietet genau das - schnelle Entwicklung, Sicherheit und eine breite Palette von Tools wie Tracy und Latte, die den Prozess vereinfachen. Installieren Sie einfach ein paar Nette-Pakete, und die Erstellung einer solchen Microsite wird zum Kinderspiel. Und Sie wissen, dass es keine versteckten Sicherheitslücken gibt. diff --git a/best-practices/de/pagination.texy b/best-practices/de/pagination.texy index 5775e52df1..ccc47f2683 100644 --- a/best-practices/de/pagination.texy +++ b/best-practices/de/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository In den Presenter injizieren wir dann die Modellklasse, und in der Rendering-Methode fragen wir nach den veröffentlichten Artikeln, die wir an die Vorlage übergeben: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -In der Vorlage kümmern wir uns um das Rendern einer Artikelliste: +Die Vorlage `default.latte` kümmert sich dann um die Auflistung der Artikel: ```latte {block content} @@ -114,7 +113,7 @@ Der nächste Schritt besteht darin, den Präsentator zu bearbeiten. Wir werden d Wir erweitern die Render-Methode auch, um die Paginator-Instanz zu erhalten, sie einzurichten und die richtigen Artikel für die Anzeige in der Vorlage auszuwählen. Der HomePresenter wird wie folgt aussehen: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ Die Vorlage iteriert bereits über Artikel auf einer Seite, fügen Sie einfach L ``` -So haben wir die Paginierung mit Paginator hinzugefügt. Wenn [Nette Database Explorer |database:explorer] anstelle von [Nette Database Core |database:core] als Datenbankschicht verwendet wird, können wir die Paginierung auch ohne Paginator implementieren. Die Klasse `Nette\Database\Table\Selection` enthält die [Page-Methode |api:Nette\Database\Table\Selection::_ page] mit der Paginierungslogik aus dem Paginator. +So haben wir die Paginierung mit Paginator hinzugefügt. Wenn [Nette Database Explorer |database:explorer] anstelle von [Nette Database Core |database:sql-way] als Datenbankschicht verwendet wird, können wir die Paginierung auch ohne Paginator implementieren. Die Klasse `Nette\Database\Table\Selection` enthält die [Page-Methode |api:Nette\Database\Table\Selection::_ page] mit der Paginierungslogik aus dem Paginator. Das Repository sieht dann wie folgt aus: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository Wir müssen keinen Paginator im Presenter erstellen, sondern verwenden die Methode des `Selection` -Objekts, das vom Repository zurückgegeben wird: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/de/post-links.texy b/best-practices/de/post-links.texy new file mode 100644 index 0000000000..13c38839bb --- /dev/null +++ b/best-practices/de/post-links.texy @@ -0,0 +1,59 @@ +Wie man POST-Links richtig verwendet +************************************ + +In Webanwendungen, insbesondere in Verwaltungsschnittstellen, sollte es eine Grundregel sein, dass Aktionen, die den Zustand des Servers verändern, nicht über die HTTP-GET-Methode durchgeführt werden sollten. Wie der Name der Methode schon sagt, sollte GET nur zum Abrufen von Daten verwendet werden, nicht zum Ändern. +Für Aktionen wie das Löschen von Datensätzen ist es angemessener, die POST-Methode zu verwenden. Ideal wäre die Verwendung der DELETE-Methode, die jedoch ohne JavaScript nicht aufgerufen werden kann, weshalb in der Regel POST verwendet wird. + +Wie macht man das in der Praxis? Verwenden Sie diesen einfachen Trick. Erstellen Sie am Anfang Ihrer Vorlage ein Hilfsformular mit dem Bezeichner `postForm`, das Sie dann für die Schaltflächen zum Löschen verwenden werden: + +```latte .{file:@layout.latte} +
    +``` + +Mit diesem Formular können Sie ein `
    @@ -36,10 +38,12 @@ Κοινό ----- - [Πώς να φορτώσετε το αρχείο ρυθμίσεων |bootstrap:] +- [Πώς να γράφετε microsites |microsites] - [Γιατί η Nette χρησιμοποιεί τη σημειογραφία σταθερών PascalCase; |https://blog.nette.org/el/gia-ligoteres-krauges-ston-kodika] - [Γιατί η Nette δεν χρησιμοποιεί την κατάληξη Interface; |https://blog.nette.org/el/ta-prothemata-kai-ta-epithemata-den-anekoun-sta-onomata-diasyndeses] - [Συμβουλές χρήσης του Composer |composer] - [Συμβουλές για επεξεργαστές & εργαλεία |editors-and-tools] +- [Εισαγωγή στον αντικειμενοστραφή προγραμματισμό |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/el/attribute-requires.texy b/best-practices/el/attribute-requires.texy new file mode 100644 index 0000000000..95d1417ac9 --- /dev/null +++ b/best-practices/el/attribute-requires.texy @@ -0,0 +1,179 @@ +Πώς να χρησιμοποιήσετε το `#[Requires]` Attribute +************************************************* + +.[perex] +Κατά τη συγγραφή μιας εφαρμογής ιστού, συναντάτε συχνά την ανάγκη να περιορίσετε την πρόσβαση σε ορισμένα τμήματα της εφαρμογής σας. Ίσως θέλετε κάποια αιτήματα να μπορούν να στέλνουν δεδομένα μόνο μέσω μιας φόρμας (χρησιμοποιώντας έτσι τη μέθοδο POST) ή να είναι προσβάσιμα μόνο σε κλήσεις AJAX. Στο Nette Framework 3.2, έχει εισαχθεί ένα νέο εργαλείο που σας επιτρέπει να ορίσετε τέτοιους περιορισμούς με κομψότητα και σαφήνεια: το `#[Requires]` χαρακτηριστικό. + +Το χαρακτηριστικό είναι ένας ειδικός δείκτης στην PHP, τον οποίο προσθέτετε πριν από τον ορισμό μιας κλάσης ή μεθόδου. Εφόσον πρόκειται ουσιαστικά για μια κλάση, πρέπει να συμπεριλάβετε τη ρήτρα use για να λειτουργήσουν τα παρακάτω παραδείγματα: + +```php +use Nette\Application\Attributes\Requires; +``` + +Μπορείτε να χρησιμοποιήσετε το `#[Requires]` με την ίδια την κλάση presenter και σε αυτές τις μεθόδους: + +- `action()` +- `render()` +- `handle()` +- `createComponent()` + +Οι δύο τελευταίες μέθοδοι αφορούν επίσης συστατικά, οπότε μπορείτε να χρησιμοποιήσετε το χαρακτηριστικό και με αυτά. + +Εάν δεν πληρούνται οι συνθήκες που καθορίζονται από το χαρακτηριστικό, ενεργοποιείται ένα σφάλμα HTTP 4xx. + + +Μέθοδοι HTTP .[#toc-http-methods] +--------------------------------- + +Μπορείτε να καθορίσετε ποιες μέθοδοι 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-method-check]. + + +Κλήσεις AJAX .[#toc-ajax-calls] +------------------------------- + +Αν θέλετε ένας παρουσιαστής ή μια μέθοδος να είναι προσβάσιμη μόνο για αιτήσεις AJAX, χρησιμοποιήστε: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +Ίδια προέλευση .[#toc-same-origin] +---------------------------------- + +Για να ενισχύσετε την ασφάλεια, μπορείτε να απαιτήσετε η αίτηση να γίνεται από τον ίδιο τομέα. Αυτό αποτρέπει την [ευπάθεια σε 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 .[#toc-access-via-forward] +------------------------------------------------ + +Μερικές φορές είναι χρήσιμο να περιορίσετε την πρόσβαση σε έναν παρουσιαστή, ώστε να είναι διαθέσιμος μόνο έμμεσα, για παράδειγμα, χρησιμοποιώντας τις μεθόδους `forward()` ή `switch()` από έναν άλλο παρουσιαστή. Με αυτόν τον τρόπο προστατεύονται οι παρουσιαστές σφαλμάτων, ώστε να μην μπορούν να ενεργοποιηθούν από μια διεύθυνση URL: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\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 + { + } +} +``` + + +Ειδικές δράσεις .[#toc-specific-actions] +---------------------------------------- + +Μπορείτε επίσης να περιορίσετε ότι ορισμένος κώδικας, όπως η δημιουργία ενός στοιχείου, θα είναι προσβάσιμος μόνο για συγκεκριμένες ενέργειες στον παρουσιαστή: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +Για μια μεμονωμένη ενέργεια, δεν χρειάζεται να γράψετε έναν πίνακα: `#[Requires(actions: 'default')]` + + +Προσαρμοσμένα χαρακτηριστικά .[#toc-custom-attributes] +------------------------------------------------------ + +Αν θέλετε να χρησιμοποιήσετε το `#[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 +{ +} +``` + + +Συμπέρασμα .[#toc-conclusion] +----------------------------- + +Το `#[Requires]` σας δίνει μεγάλη ευελιξία και έλεγχο στον τρόπο πρόσβασης στις ιστοσελίδες σας. Χρησιμοποιώντας απλούς, αλλά ισχυρούς κανόνες, μπορείτε να ενισχύσετε την ασφάλεια και την ορθή λειτουργία της εφαρμογής σας. Όπως μπορείτε να δείτε, η χρήση χαρακτηριστικών στη Nette μπορεί όχι μόνο να απλοποιήσει την εργασία σας αλλά και να την ασφαλίσει. + +{{sitename: Best Practices}} diff --git a/best-practices/el/composer.texy b/best-practices/el/composer.texy index 0971df08e3..75f9e3f0bc 100644 --- a/best-practices/el/composer.texy +++ b/best-practices/el/composer.texy @@ -142,6 +142,12 @@ composer require php 8.2.3 --no-update ``` +Αγνοώντας την έκδοση PHP .[#toc-ignoring-php-version] +===================================================== + +Τα πακέτα συνήθως καθορίζουν τόσο τη χαμηλότερη έκδοση της PHP με την οποία είναι συμβατά όσο και την υψηλότερη έκδοση με την οποία έχουν δοκιμαστεί. Αν σκοπεύετε να χρησιμοποιήσετε μια ακόμη νεότερη έκδοση της PHP, ίσως για σκοπούς δοκιμής, το Composer θα αρνηθεί να εγκαταστήσει ένα τέτοιο πακέτο. Η λύση είναι να χρησιμοποιήσετε την επιλογή `--ignore-platform-req=php+`, η οποία αναγκάζει το Composer να αγνοήσει τα ανώτερα όρια της απαιτούμενης έκδοσης PHP. + + Ψευδείς αναφορές .[#toc-false-reports] ====================================== @@ -183,7 +189,7 @@ Packagist.org - Παγκόσμιο αποθετήριο .[#toc-packagist-org-glo Στη συνέχεια, είναι απαραίτητο να εκτελείτε την εντολή `composer dumpautoload` με κάθε αλλαγή και να αφήνετε τους πίνακες αυτόματης φόρτωσης να αναγεννώνται. Αυτό είναι εξαιρετικά άβολο και είναι πολύ καλύτερο να αναθέσετε αυτή την εργασία στο [RobotLoader |robot-loader:], το οποίο εκτελεί την ίδια δραστηριότητα αυτόματα στο παρασκήνιο και πολύ πιο γρήγορα. -Η δεύτερη επιλογή είναι να ακολουθήσετε το [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Με απλά λόγια, πρόκειται για ένα σύστημα όπου τα namespaces και τα ονόματα των κλάσεων αντιστοιχούν στη δομή των καταλόγων και των ονομάτων των αρχείων, δηλαδή το `App\Router\RouterFactory` βρίσκεται στο αρχείο `/path/to/App/Router/RouterFactory.php`. Παράδειγμα διαμόρφωσης: +Η δεύτερη επιλογή είναι να ακολουθήσετε το [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Με απλά λόγια, πρόκειται για ένα σύστημα όπου τα namespaces και τα ονόματα των κλάσεων αντιστοιχούν στη δομή των καταλόγων και των ονομάτων των αρχείων, δηλαδή το `App\Core\RouterFactory` βρίσκεται στο αρχείο `/path/to/App/Core/RouterFactory.php`. Παράδειγμα διαμόρφωσης: ```js { diff --git a/best-practices/el/dynamic-snippets.texy b/best-practices/el/dynamic-snippets.texy index 59031461f0..c75bfa12dd 100644 --- a/best-practices/el/dynamic-snippets.texy +++ b/best-practices/el/dynamic-snippets.texy @@ -35,7 +35,7 @@ Template: Ajaxization .[#toc-ajaxization] =============================== -Ας φέρουμε τώρα το AJAX σε αυτή την απλή εφαρμογή. Η αλλαγή της βαθμολογίας ενός άρθρου δεν είναι αρκετά σημαντική ώστε να απαιτεί ένα αίτημα HTTP με ανακατεύθυνση, οπότε ιδανικά θα πρέπει να γίνεται με AJAX στο παρασκήνιο. Θα χρησιμοποιήσουμε το [σενάριο χειρισμού από τα πρόσθετα |https://componette.org/vojtech-dobes/nette.ajax.js/] με τη συνήθη σύμβαση ότι οι σύνδεσμοι AJAX έχουν την κλάση CSS `ajax`. +Ας φέρουμε τώρα το AJAX σε αυτή την απλή εφαρμογή. Η αλλαγή της βαθμολογίας ενός άρθρου δεν είναι αρκετά σημαντική ώστε να απαιτεί ένα αίτημα HTTP με ανακατεύθυνση, οπότε ιδανικά θα πρέπει να γίνεται με AJAX στο παρασκήνιο. Θα χρησιμοποιήσουμε το [σενάριο χειρισμού από τα πρόσθετα |application:ajax#toc-naja] με τη συνήθη σύμβαση ότι οι σύνδεσμοι AJAX έχουν την κλάση CSS `ajax`. Ωστόσο, πώς να το κάνουμε συγκεκριμένα; Η Nette προσφέρει 2 τρόπους: τον τρόπο με τα δυναμικά αποσπάσματα και τον τρόπο με τα συστατικά. Και οι δύο έχουν τα πλεονεκτήματα και τα μειονεκτήματά τους, γι' αυτό θα τους παρουσιάσουμε έναν προς έναν. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ class LikeControl extends Nette\Application\UI\Control ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/el/form-reuse.texy b/best-practices/el/form-reuse.texy index ec91ec79e1..3dc860e4cc 100644 --- a/best-practices/el/form-reuse.texy +++ b/best-practices/el/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -Είναι πολύ σημαντικό ότι η σύνδεση μεταξύ των κλάσεων `FormFactory` και `EditFormFactory` υλοποιείται με σύνθεση και όχι με κληρονομικότητα αντικειμένων: +Είναι πολύ σημαντικό ότι η σύνδεση μεταξύ των κλάσεων `FormFactory` και `EditFormFactory` υλοποιείται [με σύνθεση |nette:introduction-to-object-oriented-programming#composition] και όχι με [κληρονομικότητα αντικειμένων |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance]: ```php // ⛔ ΟΧΙ! Η ΚΛΗΡΟΝΟΜΙΆ ΔΕΝ ΑΝΉΚΕΙ ΕΔΏ @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory Η χρήση κληρονομικότητας σε αυτή την περίπτωση θα ήταν εντελώς αντιπαραγωγική. Θα αντιμετωπίζατε προβλήματα πολύ γρήγορα. Για παράδειγμα, αν θέλατε να προσθέσετε παραμέτρους στη μέθοδο `create()`, η PHP θα ανέφερε ένα σφάλμα ότι η υπογραφή της ήταν διαφορετική από την υπογραφή του γονέα. Ή όταν περνούσατε μια εξάρτηση στην κλάση `EditFormFactory` μέσω του κατασκευαστή. Αυτό θα προκαλούσε αυτό που ονομάζουμε [κόλαση του κατασκευαστή |dependency-injection:passing-dependencies#Constructor hell]. -Γενικά, είναι προτιμότερο να προτιμάτε τη σύνθεση από την κληρονομικότητα. +Είναι γενικά προτιμότερο να προτιμάτε τη [σύνθεση από την κληρονομικότητα |dependency-injection:faq#Why composition is preferred over inheritance]. Χειρισμός φόρμας .[#toc-form-handling] @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/el/lets-create-contact-form.texy b/best-practices/el/lets-create-contact-form.texy index aeded4979a..8cf6ab9a9f 100644 --- a/best-practices/el/lets-create-contact-form.texy +++ b/best-practices/el/lets-create-contact-form.texy @@ -39,7 +39,7 @@ class HomePresenter extends Presenter Τι γίνεται όμως αν ο χρήστης δεν συμπληρώσει κάποια πεδία; Σε αυτή την περίπτωση, θα πρέπει να τον ενημερώσουμε ότι πρόκειται για υποχρεωτικό πεδίο. Αυτό το κάναμε με τη μέθοδο `setRequired()`. Τέλος, προσθέσαμε και ένα [συμβάν |nette:glossary#events] `onSuccess`, το οποίο ενεργοποιείται αν η φόρμα υποβληθεί με επιτυχία. Στην περίπτωσή μας, καλεί τη μέθοδο `contactFormSucceeded`, η οποία αναλαμβάνει την επεξεργασία της υποβληθείσας φόρμας. Θα το προσθέσουμε αυτό στον κώδικα σε λίγο. -Αφήστε το στοιχείο `contantForm` να αποδοθεί στο πρότυπο `templates/Home/default.latte`: +Αφήστε το στοιχείο `contantForm` να αποδοθεί στο πρότυπο `Home/default.latte`: ```latte {block content} diff --git a/best-practices/el/microsites.texy b/best-practices/el/microsites.texy new file mode 100644 index 0000000000..b33d0b10fa --- /dev/null +++ b/best-practices/el/microsites.texy @@ -0,0 +1,63 @@ +Πώς να γράψετε Microsites +************************* + +Φανταστείτε ότι πρέπει να δημιουργήσετε γρήγορα έναν μικρό ιστότοπο για μια επερχόμενη εκδήλωση της εταιρείας σας. Θα πρέπει να είναι απλή, γρήγορη και χωρίς περιττές επιπλοκές. Μπορεί να νομίζετε ότι ένα τόσο μικρό έργο δεν απαιτεί ένα ισχυρό πλαίσιο. Τι γίνεται όμως αν η χρήση του πλαισίου Nette μπορεί να απλοποιήσει και να επιταχύνει σημαντικά αυτή τη διαδικασία; + +Ακόμα και όταν δημιουργείτε απλές ιστοσελίδες, δεν θέλετε να παραιτηθείτε από την άνεση. Δεν θέλετε να ανακαλύψετε ξανά τον τροχό. Νιώστε ελεύθεροι να είστε τεμπέληδες και να περιποιηθείτε τον εαυτό σας. Το πλαίσιο Nette μπορεί επίσης να χρησιμοποιηθεί άριστα ως μικροπλαίσιο. + +Πώς θα μπορούσε να μοιάζει μια τέτοια μικροσελίδα; Για παράδειγμα, ολόκληρος ο κώδικας της ιστοσελίδας μπορεί να τοποθετηθεί σε ένα μόνο αρχείο `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(); + +// να ρυθμίσετε τη δρομολόγηση +$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 nebo https://example.com/en +$router->addRoute('', 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:] πρώτα [το περιβάλλον |bootstrap:], στη συνέχεια ορίζει τις [διαδρομές |application:routing#dynamic-routing-with-callbacks] και τέλος εκτελεί την εφαρμογή. Το πλεονέκτημα είναι ότι η δεύτερη παράμετρος της συνάρτησης `addRoute()` μπορεί να είναι ένα callable που εκτελείται όταν ανοίγει η αντίστοιχη σελίδα. + + +Γιατί να χρησιμοποιήσετε τη Nette για μικροσελίδες; .[#toc-why-use-nette-for-microsites] +---------------------------------------------------------------------------------------- + +- Οι προγραμματιστές που δοκίμασαν ποτέ [το Tracy |tracy:] δεν μπορούν να φανταστούν σήμερα την κωδικοποίηση χωρίς αυτό. +- Αλλά πάνω απ' όλα, θα αξιοποιήσετε το σύστημα templating [Latte |latte:], επειδή από μόλις 2 σελίδες, θα θέλετε να διαχωρίσετε τη [διάταξη και το περιεχόμενο |latte:template-inheritance]. +- Και σίγουρα θέλετε να βασιστείτε στην [αυτόματη διαφυγή |latte:safety-first] για να αποφύγετε τα τρωτά σημεία XSS. +- Η Nette διασφαλίζει επίσης ότι σε περίπτωση σφάλματος, δεν θα εμφανίζονται ποτέ μηνύματα σφάλματος PHP, αλλά, αντίθετα, μια φιλική προς τον χρήστη σελίδα. +- Αν θέλετε να λαμβάνετε σχόλια από τους χρήστες, για παράδειγμα με τη μορφή φόρμας επικοινωνίας, μπορείτε επίσης να προσθέσετε [φόρμες |forms:] και [βάση δεδομένων |database:]. +- Μπορείτε επίσης εύκολα να έχετε συμπληρωμένες φόρμες που [αποστέλλονται μέσω ηλεκτρονικού ταχυδρομείου |mail:]. +- Ορισμένες φορές μπορεί να βρείτε την [προσωρινή αποθήκευση δεδομένων |caching:] χρήσιμη, για παράδειγμα, κατά τη λήψη και την εμφάνιση τροφοδοσιών. + +Στη σημερινή εποχή, όπου η ταχύτητα και η αποδοτικότητα είναι το κλειδί, είναι σημαντικό να έχετε εργαλεία που σας επιτρέπουν να επιτυγχάνετε αποτελέσματα χωρίς περιττές καθυστερήσεις. Το πλαίσιο Nette προσφέρει ακριβώς αυτό - γρήγορη ανάπτυξη, ασφάλεια και ένα ευρύ φάσμα εργαλείων όπως το Tracy και το Latte που απλοποιούν τη διαδικασία. Απλά εγκαταστήστε μερικά πακέτα Nette και η κατασκευή ενός τέτοιου microsite γίνεται παιχνιδάκι. Και ξέρετε ότι δεν υπάρχουν κρυφά κενά ασφαλείας. diff --git a/best-practices/el/pagination.texy b/best-practices/el/pagination.texy index 3003b38a84..f4175a0b5e 100644 --- a/best-practices/el/pagination.texy +++ b/best-practices/el/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository Στον Presenter στη συνέχεια εισάγουμε την κλάση model και στη μέθοδο render θα ζητήσουμε τα δημοσιευμένα άρθρα που θα περάσουμε στο template: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -Στο πρότυπο, θα φροντίσουμε για την απόδοση μιας λίστας άρθρων: +Το πρότυπο `default.latte` θα αναλάβει στη συνέχεια την καταχώριση των άρθρων: ```latte {block content} @@ -114,7 +113,7 @@ class ArticleRepository Επεκτείνουμε επίσης τη μέθοδο render για να λάβουμε την περίπτωση Paginator, να τη ρυθμίσουμε και να επιλέξουμε τα σωστά άρθρα που θα εμφανίζονται στο πρότυπο. Το HomePresenter θα μοιάζει με αυτό: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ class HomePresenter extends Nette\Application\UI\Presenter ``` -Αυτό είναι το πώς έχουμε προσθέσει σελιδοποίηση χρησιμοποιώντας Paginator. Εάν χρησιμοποιείται ο [Nette Database Explorer |database:explorer] αντί του [Nette Database |database:core] Core ως επίπεδο βάσης δεδομένων, είμαστε σε θέση να υλοποιήσουμε τη σελιδοποίηση ακόμη και χωρίς Paginator. Η κλάση `Nette\Database\Table\Selection` περιέχει τη μέθοδο [page |api:Nette\Database\Table\Selection::_ page] με λογική σελιδοποίησης που λαμβάνεται από το Paginator. +Αυτό είναι το πώς έχουμε προσθέσει σελιδοποίηση χρησιμοποιώντας Paginator. Εάν χρησιμοποιείται ο [Nette Database Explorer |database:explorer] αντί του [Nette Database |database:sql-way] Core ως επίπεδο βάσης δεδομένων, είμαστε σε θέση να υλοποιήσουμε τη σελιδοποίηση ακόμη και χωρίς Paginator. Η κλάση `Nette\Database\Table\Selection` περιέχει τη μέθοδο [page |api:Nette\Database\Table\Selection::_ page] με λογική σελιδοποίησης που λαμβάνεται από το Paginator. Το αποθετήριο θα έχει την εξής μορφή: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository Δεν χρειάζεται να δημιουργήσουμε Paginator στο Presenter, αντίθετα θα χρησιμοποιήσουμε τη μέθοδο του αντικειμένου `Selection` που επιστρέφεται από το αποθετήριο: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/el/post-links.texy b/best-practices/el/post-links.texy new file mode 100644 index 0000000000..634091cf38 --- /dev/null +++ b/best-practices/el/post-links.texy @@ -0,0 +1,59 @@ +Πώς να χρησιμοποιείτε σωστά τους συνδέσμους POST +************************************************ + +Στις εφαρμογές ιστού, ειδικά στις διαχειριστικές διεπαφές, θα πρέπει να αποτελεί βασικό κανόνα ότι οι ενέργειες που αλλάζουν την κατάσταση του διακομιστή δεν θα πρέπει να εκτελούνται μέσω της μεθόδου HTTP GET. Όπως υποδηλώνει το όνομα της μεθόδου, η GET θα πρέπει να χρησιμοποιείται μόνο για την ανάκτηση δεδομένων και όχι για την αλλαγή τους. +Για ενέργειες όπως η διαγραφή εγγραφών, είναι καταλληλότερο να χρησιμοποιείται η μέθοδος POST. Αν και το ιδανικό θα ήταν να χρησιμοποιείται η μέθοδος DELETE, αυτή δεν μπορεί να κληθεί χωρίς JavaScript, γι' αυτό και χρησιμοποιείται ιστορικά η μέθοδος POST. + +Πώς να το κάνετε στην πράξη; Χρησιμοποιήστε αυτό το απλό τέχνασμα. Στην αρχή του προτύπου σας, δημιουργήστε μια βοηθητική φόρμα με το αναγνωριστικό `postForm`, την οποία στη συνέχεια θα χρησιμοποιήσετε για τα κουμπιά διαγραφής: + +```latte .{file:@layout.latte} +
    +``` + +Με αυτή τη φόρμα, μπορείτε να χρησιμοποιήσετε ένα `
    @@ -36,10 +38,12 @@ Forms Common ------ - [How to load configuration file |bootstrap:] +- [How to write microsites |microsites] - [Why Nette uses PascalCase constant notation? |https://blog.nette.org/en/for-less-screaming-in-the-code] - [Why Nette doesn't use the Interface suffix? |https://blog.nette.org/en/prefixes-and-suffixes-do-not-belong-in-interface-names] - [Composer usage tips |composer] - [Tips on editors & tools |editors-and-tools] +- [Introduction to object-oriented programming |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/en/attribute-requires.texy b/best-practices/en/attribute-requires.texy new file mode 100644 index 0000000000..514355ad79 --- /dev/null +++ b/best-practices/en/attribute-requires.texy @@ -0,0 +1,179 @@ +How to Use the `#[Requires]` Attribute +************************************** + +.[perex] +When writing a web application, you often encounter the need to restrict access to certain parts of your application. Perhaps you want some requests to only be able to send data via a form (thus using the POST method) or to be accessible only to AJAX calls. In Nette Framework 3.2, a new tool has been introduced that allows you to set such restrictions elegantly and clearly: the `#[Requires]` attribute. + +The attribute is a special marker in PHP, which you add before the definition of a class or method. Since it is essentially a class, you need to include the use clause to make the following examples work: + +```php +use Nette\Application\Attributes\Requires; +``` + +You can use the `#[Requires]` attribute with the presenter class itself and on these methods: + +- `action()` +- `render()` +- `handle()` +- `createComponent()` + +The last two methods also concern components, so you can use the attribute with them as well. + +If the conditions specified by the attribute are not met, an HTTP 4xx error is triggered. + + +HTTP Methods +------------ + +You can specify which HTTP methods (such as GET, POST, etc.) are allowed for access. For example, if you want to allow access only by submitting a form, set: + +```php +class AdminPresenter extends Nette\Application\UI\Presenter +{ + #[Requires(methods: 'POST')] + public function actionDelete(int $id): void + { + } +} +``` + +Why should you use POST instead of GET for state changing actions and how to do it? [Read the guide |post-links]. + +You can specify a method or an array of methods. A special case is the value `'*'` to enable all methods, which presenters do not allow by default for [security reasons |application:presenters#http-method-check]. + + +AJAX Calls +---------- + +If you want a presenter or method to be accessible only for AJAX requests, use: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +Same Origin +----------- + +To enhance security, you can require that the request be made from the same domain. This prevents [vulnerability to CSRF |nette:vulnerability-protection#cross-site-request-forgery-csrf]: + +```php +#[Requires(sameOrigin: true)] +class SecurePresenter extends Nette\Application\UI\Presenter +{ +} +``` + +For `handle()` methods, access from the same domain is automatically required. So, if you want to allow access from any domain, specify: + +```php +#[Requires(sameOrigin: false)] +public function handleList(): void +{ +} +``` + + +Access via Forward +------------------ + +Sometimes it is useful to restrict access to a presenter so that it is only available indirectly, for example, using the `forward()` or `switch()` methods from another presenter. This is how error-presenters are protected to prevent them from being triggered from a URL: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +In practice, it is often necessary to mark certain views that can only be accessed based on logic in the presenter. Again, so that they cannot be opened directly: + +```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 + { + } +} +``` + + +Specific Actions +---------------- + +You can also restrict that certain code, like creating a component, will be accessible only for specific actions in the presenter: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +For a single action, there's no need to write an array: `#[Requires(actions: 'default')]` + + +Custom Attributes +----------------- + +If you want to use the `#[Requires]` attribute repeatedly with the same settings, you can create your own attribute that will inherit `#[Requires]` and set it according to your needs. + +For example, `#[SingleAction]` allows access only through the `default` action: + +```php +#[Attribute] +class SingleAction extends Nette\Application\Attributes\Requires +{ + public function __construct() + { + parent::__construct(actions: 'default'); + } +} + +#[SingleAction] +class SingleActionPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Or `#[RestMethods]` will allow access via all HTTP methods used for the 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 +{ +} +``` + + +Conclusion +---------- + +The `#[Requires]` attribute gives you great flexibility and control over how your web pages are accessed. Using simple, yet powerful rules, you can enhance the security and proper functioning of your application. As you can see, using attributes in Nette can not only simplify your work but also secure it. + +{{sitename: Best Practices}} diff --git a/best-practices/en/composer.texy b/best-practices/en/composer.texy index e87a29b0f7..d8753b98bd 100644 --- a/best-practices/en/composer.texy +++ b/best-practices/en/composer.texy @@ -142,6 +142,12 @@ Or directly in the `composer.json` file: ``` +Ignoring PHP Version +==================== + +Packages typically specify both the lowest version of PHP with which they are compatible and the highest version with which they have been tested. If you plan to use an even newer version of PHP, perhaps for testing purposes, Composer will refuse to install such a package. The solution is to use the `--ignore-platform-req=php+` option, which causes Composer to ignore the upper limits of the required PHP version. + + False Reports ============= @@ -183,7 +189,7 @@ However, it is also possible to use Composer to load other classes outside the f Subsequently, it is necessary to run the command `composer dumpautoload` with each change and let the autoloading tables regenerate. This is extremely inconvenient, and it is far better to entrust this task to [RobotLoader|robot-loader:], which performs the same activity automatically in the background and much faster. -The second option is to follow [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Simply saying, it is a system where the namespaces and class names correspond to the directory structure and file names, ie `App\Router\RouterFactory` is located in the file `/path/to/App/Router/RouterFactory.php`. Configuration example: +The second option is to follow [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Simply saying, it is a system where the namespaces and class names correspond to the directory structure and file names, ie `App\Core\RouterFactory` is located in the file `/path/to/App/Core/RouterFactory.php`. Configuration example: ```js { diff --git a/best-practices/en/dynamic-snippets.texy b/best-practices/en/dynamic-snippets.texy index a0e8e09cae..9ec54ca401 100644 --- a/best-practices/en/dynamic-snippets.texy +++ b/best-practices/en/dynamic-snippets.texy @@ -35,7 +35,7 @@ Template: Ajaxization =========== -Let's now bring AJAX to this simple application. Changing the rating of an article is not important enough to require a HTTP request with redirect, so ideally it should be done with AJAX in the background. We'll use the [handler script from add-ons |https://componette.org/vojtech-dobes/nette.ajax.js/] with the usual convention that AJAX links have the CSS class `ajax`. +Let's now bring AJAX to this simple application. Changing the rating of an article is not important enough to require a HTTP request with redirect, so ideally it should be done with AJAX in the background. We'll use the [handler script from add-ons |application:ajax#toc-naja] with the usual convention that AJAX links have the CSS class `ajax`. However, how to do it specifically? Nette offers 2 ways: the dynamic snippet way and the component way. Both of them have their pros and cons, so we will show them one by one. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ Of course we will change the view template and we will have to add a factory to ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/en/form-reuse.texy b/best-practices/en/form-reuse.texy index 176a6cb783..be53a9d13b 100644 --- a/best-practices/en/form-reuse.texy +++ b/best-practices/en/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -It is very important that the binding between the `FormFactory` and `EditFormFactory` classes is implemented by composition, not object inheritance: +It is very important that the binding between the `FormFactory` and `EditFormFactory` classes is implemented [by composition |nette:introduction-to-object-oriented-programming#composition], not [object inheritance |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance]: ```php // ⛔ NO! INHERITANCE DOESN'T BELONG HERE @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory Using inheritance in this case would be completely counterproductive. You would run into problems very quickly. For example, if you wanted to add parameters to the `create()` method; PHP would report an error that its signature was different from the parent. Or when passing a dependency to the `EditFormFactory` class via the constructor. This would cause what we call [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. -In general, it is better to prefer composition over inheritance. +It is generally better to prefer [composition over inheritance |dependency-injection:faq#Why composition is preferred over inheritance]. Form Handling @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/en/lets-create-contact-form.texy b/best-practices/en/lets-create-contact-form.texy index ad2472ed15..f00cad1b75 100644 --- a/best-practices/en/lets-create-contact-form.texy +++ b/best-practices/en/lets-create-contact-form.texy @@ -39,7 +39,7 @@ As you can see, we have created two methods. The first method `createComponentCo But what if the user doesn't fill in some fields? In that case, we should let him know that it is a required field. We did this with the `setRequired()` method. Finally, we also added an [event |nette:glossary#events] `onSuccess`, which is triggered if the form is submitted successfully. In our case, it calls the `contactFormSucceeded` method , which takes care of processing the submitted form. We'll add that to the code in a moment. -Let the `contantForm` component be rendered in the `templates/Home/default.latte` template: +Let the `contantForm` component be rendered in the `Home/default.latte` template: ```latte {block content} diff --git a/best-practices/en/microsites.texy b/best-practices/en/microsites.texy new file mode 100644 index 0000000000..846ddbe96d --- /dev/null +++ b/best-practices/en/microsites.texy @@ -0,0 +1,63 @@ +How to Write Microsites +*********************** + +Imagine that you need to quickly create a small website for an upcoming event of your company. It should be simple, fast, and without unnecessary complications. You might think that such a small project doesn't require a robust framework. But what if using the Nette framework can significantly simplify and speed up this process? + +Even when creating simple websites, you don't want to give up comfort. You don't want to reinvent the wheel. Feel free to be lazy and pamper yourself. The Nette Framework can also be excellently used as a micro framework. + +What might such a microsite look like? For example, the entire code of the website can be placed in a single `index.php` file in the public folder: + +```php +enableTracy(__DIR__ . '/../log'); +$configurator->setTempDirectory(__DIR__ . '/../temp'); + +// create a DI container based on the configuration in config.neon +$configurator->addConfig(__DIR__ . '/../app/config.neon'); +$container = $configurator->createContainer(); + +// set up routing +$router = new Nette\Application\Routers\RouteList; +$container->addService('router', $router); + +// route for URL https://example.com/ +$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { + // detect browser language and redirect to URL /en or /de etc. + $supportedLangs = ['en', 'de', 'cs']; + $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); + $presenter->redirectUrl("/$lang"); +}); + +// route for URL https://example.com/cs nebo https://example.com/en +$router->addRoute('', function ($presenter, string $lang) { + // display the appropriate template, for example ../templates/en.latte + $template = $presenter->createTemplate() + ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); + return $template; +}); + +// run the application! +$container->getByType(Nette\Application\Application::class)->run(); +``` + +Everything else will be templates stored in the parent `/templates` folder. + +The PHP code in `index.php` first [sets up the environment |bootstrap:], then defines [routes|application:routing#dynamic-routing-with-callbacks], and finally runs the application. The advantage is that the second parameter of the `addRoute()` function can be a callable that is executed when the corresponding page is opened. + + +Why use Nette for Microsites? +----------------------------- + +- Developers who have ever tried [Tracy|tracy:] can't imagine coding without it today. +- But above all, you will utilize the templating system [Latte|latte:], because from just 2 pages, you will want to separate [layout and content|latte:template-inheritance]. +- And you definitely want to rely on [automatic escaping |latte:safety-first] to prevent XSS vulnerabilities. +- Nette also ensures that in case of an error, PHP error messages will never be displayed, but instead, a user-friendly page will. +- If you want to get feedback from users, for example in the form of a contact form, you can also add [forms|forms:] and [database|database:]. +- You can also easily have filled-out forms [sent by email|mail:]. +- Sometimes you might find [caching|caching:] useful, for instance, when downloading and displaying feeds. + +In today's age, where speed and efficiency are key, it's important to have tools that allow you to achieve results without unnecessary delays. The Nette framework offers just that - fast development, security, and a wide range of tools like Tracy and Latte that simplify the process. Just install a few Nette packages, and building such a microsite becomes a breeze. And you know there are no hidden security flaws. diff --git a/best-practices/en/pagination.texy b/best-practices/en/pagination.texy index c0c0007e9b..9bebd91d3e 100644 --- a/best-practices/en/pagination.texy +++ b/best-practices/en/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository In the Presenter we then inject the model class and in the render method we will ask for the published articles that we pass to the template: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -In the template, we will take care of rendering an articles list: +The `default.latte` template will then take care of listing the articles: ```latte {block content} @@ -114,7 +113,7 @@ The next step is to edit the presenter. We will forward the number of the curren We also expand the render method to get the Paginator instance, setting it up, and selecting the correct articles to display in the template. HomePresenter will look like this: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ The template already iterates over articles in one page, just add paging links: ``` -This is how we've added pagination using Paginator. If [Nette Database Explorer |database:explorer] is used instead of [Nette Database Core |database:core] as a database layer, we are able to implement paging even without Paginator. The `Nette\Database\Table\Selection` class contains the [page |api:Nette\Database\Table\Selection::_ page] method with pagination logic taken from the Paginator. +This is how we've added pagination using Paginator. If [Nette Database Explorer |database:explorer] is used instead of [Nette Database Core |database:sql-way] as a database layer, we are able to implement paging even without Paginator. The `Nette\Database\Table\Selection` class contains the [page |api:Nette\Database\Table\Selection::_ page] method with pagination logic taken from the Paginator. The repository will look like this: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository We do not have to create Paginator in the Presenter, instead we will use the method of `Selection` object returned by the repository: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/en/post-links.texy b/best-practices/en/post-links.texy new file mode 100644 index 0000000000..45af7ea028 --- /dev/null +++ b/best-practices/en/post-links.texy @@ -0,0 +1,59 @@ +How to Properly Use POST Links +****************************** + +In web applications, especially in administrative interfaces, it should be a basic rule that actions changing the state of the server should not be performed via the HTTP GET method. As the method name suggests, GET should be used only to retrieve data, not to change it. +For actions such as deleting records, it is more appropriate to use the POST method. Although the ideal would be to use the DELETE method, this cannot be invoked without JavaScript, hence POST is historically used. + +How to do it in practice? Use this simple trick. At the beginning of your template, create a helper form with the identifier `postForm`, which you will then use for the delete buttons: + +```latte .{file:@layout.latte} +
    +``` + +With this form, you can use a `
    @@ -36,10 +38,12 @@ Formularios Común ----- - [Cómo cargar el fichero de configuración |bootstrap:] +- [Cómo escribir micrositios |microsites] - [¿Por qué Nette utiliza la notación constante PascalCase? |https://blog.nette.org/es/para-menos-gritos-en-el-codigo] - [¿Por qué Nette no utiliza el sufijo Interface? |https://blog.nette.org/es/los-prefijos-y-sufijos-no-pertenecen-a-los-nombres-de-interfaz] - [Consejos de uso de Composer |composer] - [Consejos sobre editores y herramientas |editors-and-tools] +- [Introducción a la programación orientada a objetos |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/es/attribute-requires.texy b/best-practices/es/attribute-requires.texy new file mode 100644 index 0000000000..0c51ef716b --- /dev/null +++ b/best-practices/es/attribute-requires.texy @@ -0,0 +1,179 @@ +Cómo utilizar el atributo `#[Requires]` Atributo +************************************************ + +.[perex] +Cuando escribes una aplicación web, a menudo te encuentras con la necesidad de restringir el acceso a ciertas partes de tu aplicación. Tal vez desee que algunas peticiones sólo puedan enviar datos a través de un formulario (utilizando así el método POST) o que sólo sean accesibles para llamadas AJAX. En Nette Framework 3.2, se ha introducido una nueva herramienta que permite establecer estas restricciones de forma elegante y clara: el atributo `#[Requires]` atributo. + +El atributo es un marcador especial en PHP, que se añade antes de la definición de una clase o método. Dado que se trata esencialmente de una clase, es necesario incluir la cláusula use para que los siguientes ejemplos funcionen: + +```php +use Nette\Application\Attributes\Requires; +``` + +Puede utilizar el atributo `#[Requires]` con la propia clase presentadora y en estos métodos: + +- `action()` +- `render()` +- `handle()` +- `createComponent()` + +Los dos últimos métodos también afectan a los componentes, por lo que también puede utilizar el atributo con ellos. + +Si no se cumplen las condiciones especificadas por el atributo, se produce un error HTTP 4xx. + + +Métodos HTTP .[#toc-http-methods] +--------------------------------- + +Puede especificar qué métodos HTTP (como GET, POST, etc.) están permitidos para el acceso. Por ejemplo, si desea permitir el acceso sólo mediante el envío de un formulario, establezca: + +```php +class AdminPresenter extends Nette\Application\UI\Presenter +{ + #[Requires(methods: 'POST')] + public function actionDelete(int $id): void + { + } +} +``` + +¿Por qué debería utilizar POST en lugar de GET para las acciones de cambio de estado y cómo hacerlo? [Lea la guía |post-links]. + +Puede especificar un método o un conjunto de métodos. Un caso especial es el valor `'*'` para habilitar todos los métodos, que los presentadores no permiten por defecto por [motivos de seguridad |application:presenters#http-method-check]. + + +Llamadas AJAX .[#toc-ajax-calls] +-------------------------------- + +Si desea que un presentador o método sea accesible sólo para peticiones AJAX, utilice: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +Mismo origen .[#toc-same-origin] +-------------------------------- + +Para mejorar la seguridad, puede exigir que la solicitud se realice desde el mismo dominio. Esto evita la [vulnerabilidad a CSRF |nette:vulnerability-protection#cross-site-request-forgery-csrf]: + +```php +#[Requires(sameOrigin: true)] +class SecurePresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Para los métodos `handle()` se requiere automáticamente el acceso desde el mismo dominio. Por lo tanto, si desea permitir el acceso desde cualquier dominio, especifique: + +```php +#[Requires(sameOrigin: false)] +public function handleList(): void +{ +} +``` + + +Acceso a través de Forward .[#toc-access-via-forward] +----------------------------------------------------- + +A veces es útil restringir el acceso a un presentador para que sólo esté disponible indirectamente, por ejemplo, utilizando los métodos `forward()` o `switch()` desde otro presentador. Así es como se protegen los presentadores de errores para evitar que se activen desde una URL: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +En la práctica, a menudo es necesario marcar determinadas vistas a las que sólo se puede acceder basándose en la lógica del presentador. De nuevo, para que no puedan abrirse directamente: + +```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 + { + } +} +``` + + +Acciones específicas .[#toc-specific-actions] +--------------------------------------------- + +También puede restringir que cierto código, como la creación de un componente, sea accesible sólo para acciones específicas en el presentador: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +Para una sola acción, no es necesario escribir una matriz: `#[Requires(actions: 'default')]` + + +Atributos personalizados .[#toc-custom-attributes] +-------------------------------------------------- + +Si desea utilizar el atributo `#[Requires]` atributo repetidamente con la misma configuración, puede crear su propio atributo que heredará `#[Requires]` y configurarlo según tus necesidades. + +Por ejemplo `#[SingleAction]` sólo permite el acceso a través de la acción `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 +{ +} +``` + +O `#[RestMethods]` permitirá el acceso a través de todos los métodos HTTP utilizados para la API 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 +{ +} +``` + + +Conclusión .[#toc-conclusion] +----------------------------- + +El atributo `#[Requires]` te ofrece una gran flexibilidad y control sobre cómo se accede a tus páginas web. Usando reglas simples, pero potentes, puedes mejorar la seguridad y el buen funcionamiento de tu aplicación. Como puede ver, el uso de atributos en Nette no sólo puede simplificar su trabajo, sino también asegurarlo. + +{{sitename: Best Practices}} diff --git a/best-practices/es/composer.texy b/best-practices/es/composer.texy index c550b6a92f..86cf99808a 100644 --- a/best-practices/es/composer.texy +++ b/best-practices/es/composer.texy @@ -142,6 +142,12 @@ O directamente en el archivo `composer.json`: ``` +Ignorando la versión de PHP .[#toc-ignoring-php-version] +======================================================== + +Los paquetes suelen especificar tanto la versión más baja de PHP con la que son compatibles como la versión más alta con la que han sido probados. Si planea usar una versión aún más nueva de PHP, quizás para propósitos de prueba, Composer se rehusará a instalar dicho paquete. La solución es utilizar la opción `--ignore-platform-req=php+`, que hace que Composer ignore los límites superiores de la versión PHP requerida. + + Informes falsos .[#toc-false-reports] ===================================== @@ -183,7 +189,7 @@ Sin embargo, también es posible utilizar Composer para cargar otras clases fuer Posteriormente, es necesario ejecutar el comando `composer dumpautoload` con cada cambio y dejar que se regeneren las tablas de autocarga. Esto es extremadamente incómodo, y es mucho mejor confiar esta tarea a [RobotLoader |robot-loader:], que realiza la misma actividad automáticamente en segundo plano y mucho más rápido. -La segunda opción es seguir [PSR-4 |https://www.php-fig.org/psr/psr-4/]. En pocas palabras, se trata de un sistema en el que los espacios de nombres y los nombres de las clases se corresponden con la estructura de directorios y los nombres de los archivos, es decir, `App\Router\RouterFactory` se encuentra en el archivo `/path/to/App/Router/RouterFactory.php`. Ejemplo de configuración: +La segunda opción es seguir [PSR-4 |https://www.php-fig.org/psr/psr-4/]. En pocas palabras, se trata de un sistema en el que los espacios de nombres y los nombres de las clases se corresponden con la estructura de directorios y los nombres de los archivos, es decir, `App\Core\RouterFactory` se encuentra en el archivo `/path/to/App/Core/RouterFactory.php`. Ejemplo de configuración: ```js { diff --git a/best-practices/es/dynamic-snippets.texy b/best-practices/es/dynamic-snippets.texy index 2133b4a573..d59e1eaefe 100644 --- a/best-practices/es/dynamic-snippets.texy +++ b/best-practices/es/dynamic-snippets.texy @@ -35,7 +35,7 @@ Plantilla: Ajaxización .[#toc-ajaxization] =============================== -Llevemos ahora AJAX a esta sencilla aplicación. Cambiar la calificación de un artículo no es lo suficientemente importante como para requerir una petición HTTP con redirección, así que lo ideal sería hacerlo con AJAX en segundo plano. Usaremos el [script handler de add-ons |https://componette.org/vojtech-dobes/nette.ajax.js/] con la convención habitual de que los enlaces AJAX tienen la clase CSS `ajax`. +Llevemos ahora AJAX a esta sencilla aplicación. Cambiar la calificación de un artículo no es lo suficientemente importante como para requerir una petición HTTP con redirección, así que lo ideal sería hacerlo con AJAX en segundo plano. Usaremos el [script handler de add-ons |application:ajax#toc-naja] con la convención habitual de que los enlaces AJAX tienen la clase CSS `ajax`. Sin embargo, ¿cómo hacerlo específicamente? Nette ofrece 2 maneras: la del fragmento dinámico y la del componente. Ambas tienen sus pros y sus contras, así que las mostraremos una a una. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ Por supuesto cambiaremos la plantilla de la vista y tendremos que añadir una f ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/es/form-reuse.texy b/best-practices/es/form-reuse.texy index 12e62fa802..7f48acaea8 100644 --- a/best-practices/es/form-reuse.texy +++ b/best-practices/es/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -Es muy importante que la unión entre las clases `FormFactory` y `EditFormFactory` se implemente por composición, no por herencia de objetos: +Es muy importante que la unión entre las clases `FormFactory` y `EditFormFactory` se implemente [por composición |nette:introduction-to-object-oriented-programming#composition], no por [herencia de objetos |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance]: ```php // ⛔ ¡NO! LA HERENCIA NO PERTENECE AQUÍ @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory Utilizar la herencia en este caso sería totalmente contraproducente. Se encontraría con problemas muy rápidamente. Por ejemplo, si quisiera agregar parámetros al método `create()`; PHP reportaría un error de que su firma es diferente a la del padre. O al pasar una dependencia a la clase `EditFormFactory` a través del constructor. Esto causaría lo que llamamos el infierno del [constructor |dependency-injection:passing-dependencies#Constructor hell]. -En general, es mejor preferir la composición a la herencia. +En general, es mejor preferir la [composición a la herencia |dependency-injection:faq#Why composition is preferred over inheritance]. Manejo de Formularios .[#toc-form-handling] @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/es/lets-create-contact-form.texy b/best-practices/es/lets-create-contact-form.texy index 2c7e155167..142740037a 100644 --- a/best-practices/es/lets-create-contact-form.texy +++ b/best-practices/es/lets-create-contact-form.texy @@ -39,7 +39,7 @@ Como puedes ver, hemos creado dos métodos. El primer método `createComponentCo Pero, ¿qué pasa si el usuario no rellena algunos campos? En ese caso, debemos hacerle saber que se trata de un campo obligatorio. Hicimos esto con el método `setRequired()`. Por último, también añadimos un [evento |nette:glossary#events] `onSuccess`, que se activa si el formulario se envía correctamente. En nuestro caso, llama al método `contactFormSucceeded`, que se encarga de procesar el formulario enviado. Lo añadiremos al código en un momento. -Dejemos que el componente `contantForm` sea renderizado en la plantilla `templates/Home/default.latte`: +Dejemos que el componente `contantForm` sea renderizado en la plantilla `Home/default.latte`: ```latte {block content} diff --git a/best-practices/es/microsites.texy b/best-practices/es/microsites.texy new file mode 100644 index 0000000000..6899e693df --- /dev/null +++ b/best-practices/es/microsites.texy @@ -0,0 +1,63 @@ +Cómo escribir micrositios +************************* + +Imagine que necesita crear rápidamente un pequeño sitio web para un próximo evento de su empresa. Debe ser simple, rápido y sin complicaciones innecesarias. Podrías pensar que un proyecto tan pequeño no requiere un framework robusto. Pero, ¿y si el uso del framework Nette puede simplificar y acelerar significativamente este proceso? + +Incluso cuando se crean sitios web sencillos, no se quiere renunciar a la comodidad. No quieres reinventar la rueda. Siéntase libre de ser perezoso y mimarse. Nette Framework también puede utilizarse de forma excelente como micro framework. + +¿Qué aspecto podría tener un micrositio de este tipo? Por ejemplo, todo el código del sitio web puede colocarse en un único archivo `index.php` en la carpeta pública: + +```php +<?php + +require __DIR__ . '/../vendor/autoload.php'; + +$configurator = new Nette\Bootstrap\Configurator; +$configurator->enableTracy(__DIR__ . '/../log'); +$configurator->setTempDirectory(__DIR__ . '/../temp'); + +// crear un contenedor DI basado en la configuración en config.neon +$configurator->addConfig(__DIR__ . '/../app/config.neon'); +$container = $configurator->createContainer(); + +// configurar el enrutamiento +$router = new Nette\Application\Routers\RouteList; +$container->addService('router', $router); + +// ruta para la URL https://example.com/ +$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { + // detectar el idioma del navegador y redirigir a la URL /en o /de etc. + $supportedLangs = ['en', 'de', 'cs']; + $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); + $presenter->redirectUrl("/$lang"); +}); + +// ruta para URL https://example.com/cs nebo https://example.com/en +$router->addRoute('', function ($presenter, string $lang) { + // mostrar la plantilla adecuada, por ejemplo ../templates/es.latte + $template = $presenter->createTemplate() + ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); + return $template; +}); + +// ejecutar la aplicación +$container->getByType(Nette\Application\Application::class)->run(); +``` + +Todo lo demás serán plantillas almacenadas en la carpeta padre `/templates`. + +El código PHP en `index.php` primero [configura el entorno |bootstrap:], luego define las [rutas |application:routing#dynamic-routing-with-callbacks] y finalmente ejecuta la aplicación. La ventaja es que el segundo parámetro de la función `addRoute()` puede ser un callable que se ejecuta cuando se abre la página correspondiente. + + +¿Por qué utilizar Nette para los micrositios? .[#toc-why-use-nette-for-microsites] +---------------------------------------------------------------------------------- + +- Los desarrolladores que hayan probado alguna vez [Tracy |tracy:] no se imaginan hoy en día codificar sin él. +- Pero sobre todo, utilizarán el sistema de plantillas [Latte |latte:], porque a partir de sólo 2 páginas, querrán separar [maquetación y contenido |latte:template-inheritance]. +- Y sin duda querrá confiar en el [escape automático |latte:safety-first] para evitar vulnerabilidades XSS. +- Nette también garantiza que, en caso de error, nunca se mostrarán mensajes de error de PHP, sino una página fácil de usar. +- Si desea obtener comentarios de los usuarios, por ejemplo en forma de formulario de contacto, también puede añadir [formularios |forms:] y [base de datos |database:]. +- También puedes hacer que los formularios rellenados se envíen fácilmente [por correo electrónico |mail:]. +- A veces puede resultarte útil [el almacenamiento en caché |caching:], por ejemplo, al descargar y mostrar feeds. + +En la era actual, en la que la velocidad y la eficacia son fundamentales, es importante contar con herramientas que permitan obtener resultados sin retrasos innecesarios. El framework Nette ofrece precisamente eso: desarrollo rápido, seguridad y una amplia gama de herramientas como Tracy y Latte que simplifican el proceso. Basta con instalar algunos paquetes de Nette, y construir un micrositio de este tipo se convierte en un juego de niños. Y sabes que no hay fallos de seguridad ocultos. diff --git a/best-practices/es/pagination.texy b/best-practices/es/pagination.texy index 4638f53da7..9a6c3b766b 100644 --- a/best-practices/es/pagination.texy +++ b/best-practices/es/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository En el Presentador inyectamos entonces la clase modelo y en el método render pediremos los artículos publicados que pasamos al modelo: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -En la plantilla nos encargaremos de renderizar una lista de artículos: +La plantilla `default.latte` se encargará de listar los artículos: ```latte {block content} @@ -114,7 +113,7 @@ El siguiente paso es editar el presentador. Enviaremos el número de la página También expandimos el método render para obtener la instancia Paginator, configurándola, y seleccionando los artículos correctos para mostrar en la plantilla. HomePresenter tendrá este aspecto: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ La plantilla ya itera sobre los artículos en una página, sólo hay que añadir ``` -Así es como hemos añadido la paginación usando Paginator. Si se utiliza [Nette Database Explorer |database:explorer] en lugar de [Nette Database Core |database:core] como capa de base de datos, podemos implementar la paginación incluso sin Paginator. La clase `Nette\Database\Table\Selection` contiene el método [page |api:Nette\Database\Table\Selection::_ page] con la lógica de paginación tomada del Paginator. +Así es como hemos añadido la paginación usando Paginator. Si se utiliza [Nette Database Explorer |database:explorer] en lugar de [Nette Database Core |database:sql-way] como capa de base de datos, podemos implementar la paginación incluso sin Paginator. La clase `Nette\Database\Table\Selection` contiene el método [page |api:Nette\Database\Table\Selection::_ page] con la lógica de paginación tomada del Paginator. El repositorio tendrá este aspecto: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository No tenemos que crear Paginator en el Presentador, en su lugar utilizaremos el método del objeto `Selection` devuelto por el repositorio: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/es/post-links.texy b/best-practices/es/post-links.texy new file mode 100644 index 0000000000..4b13b68b11 --- /dev/null +++ b/best-practices/es/post-links.texy @@ -0,0 +1,59 @@ +Cómo utilizar correctamente los enlaces POST +******************************************** + +En aplicaciones web, especialmente en interfaces administrativas, debería ser una regla básica que las acciones que cambian el estado del servidor no deberían realizarse a través del método HTTP GET. Como el nombre del método sugiere, GET debería usarse sólo para recuperar datos, no para cambiarlos. +Para acciones como borrar registros, es más apropiado utilizar el método POST. Aunque lo ideal sería utilizar el método DELETE, éste no puede invocarse sin JavaScript, de ahí que históricamente se utilice POST. + +¿Cómo hacerlo en la práctica? Utilice este sencillo truco. Al principio de su plantilla, cree un formulario de ayuda con el identificador `postForm`, que luego utilizará para los botones de eliminación: + +```latte .{file:@layout.latte} +
    +``` + +Con este formulario, puedes utilizar un `
    @@ -36,10 +38,12 @@ Formulaires Commun ------ - [Comment charger le fichier de configuration |bootstrap:] +- [Comment rédiger des microsites |microsites] - [Pourquoi Nette utilise la notation constante PascalCase ? |https://blog.nette.org/fr/pour-moins-crier-dans-le-code] - [Pourquoi Nette n'utilise pas le suffixe Interface ? |https://blog.nette.org/fr/les-prefixes-et-les-suffixes-n-ont-pas-leur-place-dans-les-noms-d-interface] - [Conseils d'utilisation de Composer |composer] - [Conseils sur les éditeurs et les outils |editors-and-tools] +- [Introduction à la programmation orientée objet |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/fr/attribute-requires.texy b/best-practices/fr/attribute-requires.texy new file mode 100644 index 0000000000..03e5be9f2e --- /dev/null +++ b/best-practices/fr/attribute-requires.texy @@ -0,0 +1,179 @@ +Comment utiliser l'attribut `#[Requires]` Attribut +************************************************** + +.[perex] +Lorsque vous écrivez une application web, vous rencontrez souvent le besoin de restreindre l'accès à certaines parties de votre application. Vous souhaitez peut-être que certaines requêtes ne puissent envoyer des données que par l'intermédiaire d'un formulaire (en utilisant donc la méthode POST) ou qu'elles ne soient accessibles qu'aux appels AJAX. Dans le Nette Framework 3.2, un nouvel outil a été introduit qui vous permet de définir de telles restrictions de manière élégante et claire : l'attribut `#[Requires]` attribut. + +L'attribut est un marqueur spécial en PHP, que vous ajoutez avant la définition d'une classe ou d'une méthode. Comme il s'agit essentiellement d'une classe, vous devez inclure la clause use pour que les exemples suivants fonctionnent : + +```php +use Nette\Application\Attributes\Requires; +``` + +Vous pouvez utiliser l'attribut `#[Requires]` avec la classe du présentateur elle-même et avec ces méthodes : + +- `action()` +- `render()` +- `handle()` +- `createComponent()` + +Les deux dernières méthodes concernent également les composants, de sorte que vous pouvez également utiliser l'attribut avec eux. + +Si les conditions spécifiées par l'attribut ne sont pas remplies, une erreur HTTP 4xx est déclenchée. + + +Méthodes HTTP .[#toc-http-methods] +---------------------------------- + +Vous pouvez spécifier les méthodes HTTP (telles que GET, POST, etc.) autorisées pour l'accès. Par exemple, si vous souhaitez n'autoriser l'accès qu'en soumettant un formulaire, définissez : + +```php +class AdminPresenter extends Nette\Application\UI\Presenter +{ + #[Requires(methods: 'POST')] + public function actionDelete(int $id): void + { + } +} +``` + +Pourquoi utiliser POST au lieu de GET pour les actions de changement d'état et comment le faire ? [Lire le guide |post-links]. + +Vous pouvez spécifier une méthode ou un tableau de méthodes. Un cas particulier est la valeur `'*'` qui permet d'activer toutes les méthodes, ce que les présentateurs n'autorisent pas par défaut pour des [raisons de sécurité |application:presenters#http-method-check]. + + +Appels AJAX .[#toc-ajax-calls] +------------------------------ + +Si vous souhaitez qu'un présentateur ou une méthode ne soit accessible que pour les requêtes AJAX, utilisez : + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +Même origine .[#toc-same-origin] +-------------------------------- + +Pour renforcer la sécurité, vous pouvez exiger que la demande soit faite à partir du même domaine. Cela permet d'éviter la [vulnérabilité au CSRF |nette:vulnerability-protection#cross-site-request-forgery-csrf]: + +```php +#[Requires(sameOrigin: true)] +class SecurePresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Pour les méthodes `handle()` l'accès à partir du même domaine est automatiquement requis. Par conséquent, si vous souhaitez autoriser l'accès à partir de n'importe quel domaine, indiquez : + +```php +#[Requires(sameOrigin: false)] +public function handleList(): void +{ +} +``` + + +Accès via Forward .[#toc-access-via-forward] +-------------------------------------------- + +Il est parfois utile de restreindre l'accès à un présentateur pour qu'il ne soit accessible qu'indirectement, par exemple en utilisant les méthodes `forward()` ou `switch()` d'un autre présentateur. C'est ainsi que les présentateurs d'erreurs sont protégés pour éviter qu'ils ne soient déclenchés à partir d'une URL : + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Dans la pratique, il est souvent nécessaire de marquer certaines vues auxquelles on ne peut accéder qu'en fonction de la logique du présentateur. Là encore, elles ne peuvent pas être ouvertes directement : + +```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 + { + } +} +``` + + +Actions spécifiques .[#toc-specific-actions] +-------------------------------------------- + +Vous pouvez également limiter l'accès à certains codes, comme la création d'un composant, à des actions spécifiques dans le présentateur : + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +Pour une action unique, il n'est pas nécessaire d'écrire un tableau : `#[Requires(actions: 'default')]` + + +Attributs personnalisés .[#toc-custom-attributes] +------------------------------------------------- + +Si vous souhaitez utiliser l'attribut `#[Requires]` à plusieurs reprises avec les mêmes paramètres, vous pouvez créer votre propre attribut qui héritera de l'attribut `#[Requires]` et le paramétrer en fonction de vos besoins. + +Par exemple, l'attribut `#[SingleAction]` n'autorise l'accès que par l'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 +{ +} +``` + +ou `#[RestMethods]` permet l'accès via toutes les méthodes HTTP utilisées pour l'API 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 +{ +} +``` + + +Conclusion .[#toc-conclusion] +----------------------------- + +L'attribut `#[Requires]` vous offre une grande flexibilité et un contrôle sur la manière dont vos pages web sont accessibles. En utilisant des règles simples mais puissantes, vous pouvez améliorer la sécurité et le bon fonctionnement de votre application. Comme vous pouvez le constater, l'utilisation des attributs dans Nette peut non seulement simplifier votre travail, mais aussi le sécuriser. + +{{sitename: Best Practices}} diff --git a/best-practices/fr/composer.texy b/best-practices/fr/composer.texy index 485cd78e67..bb6de406f9 100644 --- a/best-practices/fr/composer.texy +++ b/best-practices/fr/composer.texy @@ -142,6 +142,12 @@ Ou directement dans le fichier `composer.json` : ``` +Ignorer la version de PHP .[#toc-ignoring-php-version] +====================================================== + +Les paquets spécifient généralement la version la plus basse de PHP avec laquelle ils sont compatibles et la version la plus haute avec laquelle ils ont été testés. Si vous prévoyez d'utiliser une version plus récente de PHP, peut-être à des fins de test, Composer refusera d'installer un tel paquet. La solution est d'utiliser l'option `--ignore-platform-req=php+`, qui permet à Composer d'ignorer les limites supérieures de la version de PHP requise. + + Faux rapports .[#toc-false-reports] =================================== @@ -183,7 +189,7 @@ Toutefois, il est également possible d'utiliser Composer pour charger d'autres Par la suite, il est nécessaire d'exécuter la commande `composer dumpautoload` à chaque modification et de laisser les tables d'autoloadage se régénérer. Ceci est extrêmement gênant, et il est de loin préférable de confier cette tâche à [RobotLoader |robot-loader:], qui effectue la même activité automatiquement en arrière-plan et beaucoup plus rapidement. -La deuxième option consiste à suivre le [système PSR-4 |https://www.php-fig.org/psr/psr-4/]. Pour faire simple, il s'agit d'un système où les espaces de noms et les noms de classes correspondent à la structure des répertoires et aux noms de fichiers, c'est-à-dire que `App\Router\RouterFactory` est situé dans le fichier `/path/to/App/Router/RouterFactory.php`. Exemple de configuration : +La deuxième option consiste à suivre le [système PSR-4 |https://www.php-fig.org/psr/psr-4/]. Pour faire simple, il s'agit d'un système où les espaces de noms et les noms de classes correspondent à la structure des répertoires et aux noms de fichiers, c'est-à-dire que `App\Core\RouterFactory` est situé dans le fichier `/path/to/App/Core/RouterFactory.php`. Exemple de configuration : ```js { diff --git a/best-practices/fr/dynamic-snippets.texy b/best-practices/fr/dynamic-snippets.texy index 5bc4743f67..053c6e6607 100644 --- a/best-practices/fr/dynamic-snippets.texy +++ b/best-practices/fr/dynamic-snippets.texy @@ -35,7 +35,7 @@ Template : Ajaxisation .[#toc-ajaxization] =============================== -Introduisons maintenant AJAX dans cette application simple. La modification de l'évaluation d'un article n'est pas assez importante pour nécessiter une requête HTTP avec redirection, donc l'idéal serait de le faire avec AJAX en arrière-plan. Nous utiliserons le [script de gestion des modules complémentaires |https://componette.org/vojtech-dobes/nette.ajax.js/] avec la convention habituelle selon laquelle les liens AJAX ont la classe CSS `ajax`. +Introduisons maintenant AJAX dans cette application simple. La modification de l'évaluation d'un article n'est pas assez importante pour nécessiter une requête HTTP avec redirection, donc l'idéal serait de le faire avec AJAX en arrière-plan. Nous utiliserons le [script de gestion des modules complémentaires |application:ajax#toc-naja] avec la convention habituelle selon laquelle les liens AJAX ont la classe CSS `ajax`. Cependant, comment le faire spécifiquement ? Nette propose deux méthodes : le snippet dynamique et le composant. Ces deux méthodes ont leurs avantages et leurs inconvénients, nous allons donc les présenter une par une. @@ -151,7 +151,7 @@ Bien sûr, nous allons modifier le modèle de vue et nous devrons ajouter une fa ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/fr/form-reuse.texy b/best-practices/fr/form-reuse.texy index a5a2829f81..97260a1ef4 100644 --- a/best-practices/fr/form-reuse.texy +++ b/best-practices/fr/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -Il est très important que la liaison entre les classes `FormFactory` et `EditFormFactory` soit mise en œuvre par composition et non par héritage d'objets : +Il est très important que la liaison entre les classes `FormFactory` et `EditFormFactory` soit mise en œuvre [par composition |nette:introduction-to-object-oriented-programming#composition] et non par [héritage d'objets |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance]: ```php // ⛔ NO ! L'HÉRITAGE N'A PAS SA PLACE ICI @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory L'utilisation de l'héritage dans ce cas serait totalement contre-productive. Vous rencontreriez des problèmes très rapidement. Par exemple, si vous vouliez ajouter des paramètres à la méthode `create()`, PHP signalerait une erreur parce que sa signature est différente de celle du parent. Ou lorsque vous passez une dépendance à la classe `EditFormFactory` via le constructeur. Cela provoquerait ce que nous appelons l'[enfer du constructeur |dependency-injection:passing-dependencies#Constructor hell]. -En général, il est préférable de préférer la composition à l'héritage. +Il est généralement préférable de préférer la [composition à l'héritage |dependency-injection:faq#Why composition is preferred over inheritance]. Traitement des formulaires .[#toc-form-handling] @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/fr/lets-create-contact-form.texy b/best-practices/fr/lets-create-contact-form.texy index 50afc50327..0b3b5b4409 100644 --- a/best-practices/fr/lets-create-contact-form.texy +++ b/best-practices/fr/lets-create-contact-form.texy @@ -39,7 +39,7 @@ Comme vous pouvez le voir, nous avons créé deux méthodes. La première métho Mais que se passe-t-il si l'utilisateur ne remplit pas certains champs ? Dans ce cas, nous devons l'informer qu'il s'agit d'un champ obligatoire. C'est ce que nous avons fait avec la méthode `setRequired()`. Enfin, nous avons également ajouté un [événement |nette:glossary#events] `onSuccess`, qui est déclenché si le formulaire est soumis avec succès. Dans notre cas, il appelle la méthode `contactFormSucceeded`, qui se charge de traiter le formulaire soumis. Nous l'ajouterons au code dans un instant. -Laissez le composant `contantForm` être rendu dans le modèle `templates/Home/default.latte`: +Laissez le composant `contantForm` être rendu dans le modèle `Home/default.latte`: ```latte {block content} diff --git a/best-practices/fr/microsites.texy b/best-practices/fr/microsites.texy new file mode 100644 index 0000000000..cdc28d162b --- /dev/null +++ b/best-practices/fr/microsites.texy @@ -0,0 +1,63 @@ +Comment rédiger des microsites +****************************** + +Imaginez que vous ayez besoin de créer rapidement un petit site web pour un événement à venir de votre entreprise. Il doit être simple, rapide et sans complications inutiles. Vous pourriez penser qu'un si petit projet ne nécessite pas un framework robuste. Et si l'utilisation du framework Nette pouvait considérablement simplifier et accélérer ce processus ? + +Même lors de la création de sites web simples, vous ne voulez pas renoncer au confort. Vous ne voulez pas réinventer la roue. N'hésitez pas à être paresseux et à vous faire plaisir. Le cadre Nette peut également être utilisé de manière optimale en tant que micro-cadre. + +À quoi pourrait ressembler un tel microsite ? Par exemple, l'ensemble du code du site web peut être placé dans un seul fichier `index.php` dans le dossier public : + +```php +<?php + +require __DIR__ . '/../vendor/autoload.php'; + +$configurator = new Nette\Bootstrap\Configurator; +$configurator->enableTracy(__DIR__ . '/../log'); +$configurator->setTempDirectory(__DIR__ . '/../temp'); + +// créer un conteneur DI basé sur la configuration dans config.neon +$configurator->addConfig(__DIR__ . '/../app/config.neon'); +$container = $configurator->createContainer(); + +// mettre en place le routage +$router = new Nette\Application\Routers\RouteList; +$container->addService('router', $router); + +// route pour l'URL https://example.com/ +$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { + // détecter la langue du navigateur et rediriger vers l'URL /en ou /de etc. + $supportedLangs = ['en', 'de', 'cs']; + $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); + $presenter->redirectUrl("/$lang"); +}); + +// route pour l'URL https://example.com/cs nebo https://example.com/en +$router->addRoute('', function ($presenter, string $lang) { + // affiche le modèle approprié, par exemple ../templates/en.latte + $template = $presenter->createTemplate() + ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); + return $template; +}); + +// Exécutez l'application ! +$container->getByType(Nette\Application\Application::class)->run(); +``` + +Tout le reste sera des modèles stockés dans le dossier parent `/templates`. + +Le code PHP dans `index.php` configure d'abord l'[environnement |bootstrap:], puis définit les [itinéraires |application:routing#dynamic-routing-with-callbacks] et enfin exécute l'application. L'avantage est que le deuxième paramètre de la fonction `addRoute()` peut être un appel qui est exécuté lorsque la page correspondante est ouverte. + + +Pourquoi utiliser Nette pour les microsites ? .[#toc-why-use-nette-for-microsites] +---------------------------------------------------------------------------------- + +- Les développeurs qui ont déjà essayé [Tracy |tracy:] ne peuvent imaginer coder sans lui aujourd'hui. +- Mais surtout, vous utiliserez le système de templates [Latte |latte:], parce qu'à partir de 2 pages seulement, vous voudrez séparer la [mise en page et le contenu |latte:template-inheritance]. +- Et vous voudrez certainement compter sur l'[échappement automatique |latte:safety-first] pour éviter les vulnérabilités XSS. +- Nette garantit également qu'en cas d'erreur, les messages d'erreur PHP ne seront jamais affichés, mais qu'une page conviviale le sera. +- Si vous souhaitez obtenir un retour d'information de la part des utilisateurs, par exemple sous la forme d'un formulaire de contact, vous pouvez également ajouter des [formulaires |forms:] et une [base de données |database:]. +- Vous pouvez également faire en sorte que les formulaires remplis soient [envoyés par courrier électronique |mail:]. +- La [mise en cache |caching:] peut parfois s'avérer utile, par exemple lors du téléchargement et de l'affichage de flux. + +À notre époque, où la rapidité et l'efficacité sont essentielles, il est important de disposer d'outils qui vous permettent d'obtenir des résultats sans retards inutiles. C'est précisément ce qu'offre le cadre Nette : un développement rapide, la sécurité et une large gamme d'outils tels que Tracy et Latte qui simplifient le processus. Il suffit d'installer quelques paquets Nette pour que la création d'un microsite devienne un jeu d'enfant. Et vous savez qu'il n'y a pas de failles de sécurité cachées. diff --git a/best-practices/fr/pagination.texy b/best-practices/fr/pagination.texy index 9cb7e34af0..cc208a111d 100644 --- a/best-practices/fr/pagination.texy +++ b/best-practices/fr/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository Dans le Presenter nous injectons ensuite la classe model et dans la méthode render nous allons demander les articles publiés que nous passons au template : ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -Dans le modèle, nous allons nous occuper de rendre une liste d'articles : +Le modèle `default.latte` se chargera ensuite de répertorier les articles : ```latte {block content} @@ -114,7 +113,7 @@ L'étape suivante consiste à modifier le présentateur. Nous allons transmettre Nous étendons également la méthode de rendu pour obtenir l'instance de Paginator, la configurer et sélectionner les bons articles à afficher dans le modèle. Le HomePresenter ressemblera à ceci : ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ Le modèle fait déjà défiler les articles sur une page, il suffit d'ajouter d ``` -C'est ainsi que nous avons ajouté la pagination en utilisant Paginator. Si [Nette Database Explorer |database:explorer] est utilisé à la place de [Nette Database Core |database:core] comme couche de base de données, nous sommes capables d'implémenter la pagination même sans Paginator. La classe `Nette\Database\Table\Selection` contient la méthode [page |api:Nette\Database\Table\Selection::_ page] avec la logique de pagination prise dans le Paginator. +C'est ainsi que nous avons ajouté la pagination en utilisant Paginator. Si [Nette Database Explorer |database:explorer] est utilisé à la place de [Nette Database Core |database:sql-way] comme couche de base de données, nous sommes capables d'implémenter la pagination même sans Paginator. La classe `Nette\Database\Table\Selection` contient la méthode [page |api:Nette\Database\Table\Selection::_ page] avec la logique de pagination prise dans le Paginator. Le référentiel ressemblera à ceci : @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository Nous n'avons pas besoin de créer le Paginator dans le Presenter, à la place nous utiliserons la méthode de l'objet `Selection` retourné par le référentiel : ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/fr/post-links.texy b/best-practices/fr/post-links.texy new file mode 100644 index 0000000000..70211882bc --- /dev/null +++ b/best-practices/fr/post-links.texy @@ -0,0 +1,59 @@ +Comment utiliser correctement les liens POST +******************************************** + +Dans les applications web, en particulier dans les interfaces administratives, la règle de base devrait être que les actions modifiant l'état du serveur ne doivent pas être effectuées via la méthode HTTP GET. Comme le nom de la méthode le suggère, GET ne doit être utilisé que pour récupérer des données, et non pour les modifier. +Pour des actions telles que la suppression d'enregistrements, il est plus approprié d'utiliser la méthode POST. L'idéal serait d'utiliser la méthode DELETE, mais celle-ci ne peut être invoquée sans JavaScript, d'où l'utilisation historique de POST. + +Comment faire en pratique ? Utilisez cette astuce simple. Au début de votre modèle, créez un formulaire d'aide avec l'identifiant `postForm`, que vous utiliserez ensuite pour les boutons de suppression : + +```latte .{file:@layout.latte} +
    +``` + +Avec ce formulaire, vous pouvez utiliser un `
    @@ -36,10 +38,12 @@ Nyomtatványok Közös ----- - [Konfigurációs fájl betöltése |bootstrap:] +- [Hogyan írjunk microsite-okat |microsites] - [Miért használ a Nette PascalCase konstans jelölést? |https://blog.nette.org/hu/kevesebb-sikolyert-a-kodban] - [Miért nem használja a Nette az Interface utótagot? |https://blog.nette.org/hu/az-elotagok-es-utotagok-nem-tartoznak-az-interfesznevekbe] - [Composer használati tippek |composer] - [Tippek a szerkesztőkhöz és eszközökhöz |editors-and-tools] +- [Bevezetés az objektumorientált programozásba |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/hu/attribute-requires.texy b/best-practices/hu/attribute-requires.texy new file mode 100644 index 0000000000..22cea565bb --- /dev/null +++ b/best-practices/hu/attribute-requires.texy @@ -0,0 +1,179 @@ +Hogyan használjuk a `#[Requires]` Attribútum +******************************************** + +.[perex] +Webalkalmazások írása során gyakran találkozunk azzal az igénnyel, hogy korlátozni kell az alkalmazás bizonyos részeihez való hozzáférést. Talán azt szeretné, hogy egyes kérések csak űrlapon keresztül küldhessenek adatokat (tehát a POST módszerrel), vagy csak AJAX-hívások számára legyenek elérhetők. A Nette Framework 3.2-ben egy új eszköz került bevezetésre, amely lehetővé teszi az ilyen korlátozások elegáns és egyértelmű beállítását: a `#[Requires]` attribútum. + +Az attribútum egy speciális jelölő a PHP-ban, amelyet egy osztály vagy metódus definíciója előtt adunk meg. Mivel lényegében egy osztályról van szó, a következő példák működéséhez a use záradékot is be kell illesztenie: + +```php +use Nette\Application\Attributes\Requires; +``` + +Használhatja a `#[Requires]` attribútumot magával a prezentáló osztállyal és ezekkel a metódusokkal: + +- `action()` +- `render()` +- `handle()` +- `createComponent()` + +Az utolsó két módszer szintén komponensekre vonatkozik, így ezekkel is használhatja az attribútumot. + +Ha az attribútum által meghatározott feltételek nem teljesülnek, a rendszer egy HTTP 4xx hibaüzenetet küld. + + +HTTP-módszerek .[#toc-http-methods] +----------------------------------- + +Megadhatja, hogy mely HTTP-módszerek (például GET, POST stb.) engedélyezettek a hozzáféréshez. Ha például csak egy űrlap elküldésével akarja engedélyezni a hozzáférést, állítsa be a következőket: + +```php +class AdminPresenter extends Nette\Application\UI\Presenter +{ + #[Requires(methods: 'POST')] + public function actionDelete(int $id): void + { + } +} +``` + +Miért érdemes POST-ot használni GET helyett állapotváltoztató műveletekhez, és hogyan kell ezt megtenni? [Olvassa el az útmutatót |post-links]. + +Megadhat egy módszert vagy módszerek tömbjét. Speciális eset a `'*'` érték, amely minden módszert engedélyez, amit a prezenterek [biztonsági okokból |application:presenters#http-method-check] alapértelmezés szerint nem engedélyeznek. + + +AJAX hívások .[#toc-ajax-calls] +------------------------------- + +Ha azt szeretné, hogy egy bemutató vagy metódus csak AJAX-kérések számára legyen elérhető, használja a következőt: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +Ugyanaz az eredet .[#toc-same-origin] +------------------------------------- + +A biztonság növelése érdekében megkövetelheti, hogy a kérés ugyanarról a tartományról érkezzen. Ez megakadályozza [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 oldalon. `handle()` módszerek esetében automatikusan ugyanabból a tartományból való hozzáférés szükséges. Ha tehát bármilyen tartományból engedélyezni szeretné a hozzáférést, adja meg: + +```php +#[Requires(sameOrigin: false)] +public function handleList(): void +{ +} +``` + + +Hozzáférés a Forwardon keresztül .[#toc-access-via-forward] +----------------------------------------------------------- + +Néha célszerű úgy korlátozni a hozzáférést egy prezentálóhoz, hogy az csak közvetve legyen elérhető, például egy másik prezentáló `forward()` vagy `switch()` módszereivel. A hiba-bemutatókat így védik, hogy megakadályozzák, hogy egy URL-ből indítsák őket: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +A gyakorlatban gyakran szükség van bizonyos nézetek megjelölésére, amelyek csak a prezenter logikája alapján érhetők el. Megint csak azért, 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 intézkedések .[#toc-specific-actions] +--------------------------------------------- + +Azt is korlátozhatja, hogy bizonyos kódok, például egy komponens létrehozása, csak bizonyos műveletek esetén legyenek elérhetők a prezenterben: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +Egyetlen művelethez nem szükséges tömböt írni: `#[Requires(actions: 'default')]` + + +Egyéni attribútumok .[#toc-custom-attributes] +--------------------------------------------- + +Ha a `#[Requires]` attribútumot ismételten ugyanazokkal a beállításokkal használni, létrehozhat saját attribútumot, amely örökli a `#[Requires]` és az igényeinek megfelelően állíthatja be. + +Például, `#[SingleAction]` csak a `default` műveleten 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 `#[RestMethods]` lehetővé teszi a hozzáférést a REST API-hoz használt összes HTTP-módszeren 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 .[#toc-conclusion] +-------------------------------- + +A `#[Requires]` attribútum nagyfokú rugalmasságot és ellenőrzést biztosít a weboldalak elérésének módját illetően. Egyszerű, de hatékony szabályok használatával fokozhatja az alkalmazás biztonságát és megfelelő működését. Amint láthatja, az attribútumok használata a Nette-ben nemcsak egyszerűsítheti, hanem biztosíthatja is a munkáját. + +{{sitename: Best Practices}} diff --git a/best-practices/hu/composer.texy b/best-practices/hu/composer.texy index a4159ed069..d3c767b633 100644 --- a/best-practices/hu/composer.texy +++ b/best-practices/hu/composer.texy @@ -142,6 +142,12 @@ Vagy közvetlenül a `composer.json` fájlban: ``` +PHP verzió figyelmen kívül hagyása .[#toc-ignoring-php-version] +=============================================================== + +A csomagok általában megadják a PHP legalacsonyabb verzióját, amellyel kompatibilisek, és a legmagasabb verzióját, amellyel tesztelték őket. Ha a PHP egy még újabb verzióját tervezi használni, esetleg tesztelési céllal, a Composer elutasítja az ilyen csomag telepítését. A megoldás a `--ignore-platform-req=php+` opció használata, amelynek hatására a Composer figyelmen kívül hagyja a szükséges PHP-verzió felső határértékeit. + + Hamis jelentések .[#toc-false-reports] ====================================== @@ -183,7 +189,7 @@ Lehetőség van azonban arra is, hogy a Composer segítségével a `vendor` mapp Ezt követően minden egyes változtatásnál el kell indítani a `composer dumpautoload` parancsot, és hagyni kell, hogy az autoloading táblák újratermelődjenek. Ez rendkívül kényelmetlen, és sokkal jobb, ha ezt a feladatot a [RobotLoaderre |robot-loader:] bízzuk, amely a háttérben automatikusan és sokkal gyorsabban végzi el ugyanezt a tevékenységet. -A második lehetőség a [PSR-4 |https://www.php-fig.org/psr/psr-4/] követése. Egyszerűen fogalmazva, ez egy olyan rendszer, ahol a névterek és az osztálynevek megfelelnek a könyvtárszerkezetnek és a fájlneveknek, azaz a `App\Router\RouterFactory` a `/path/to/App/Router/RouterFactory.php` fájlban található. Konfigurációs példa: +A második lehetőség a [PSR-4 |https://www.php-fig.org/psr/psr-4/] követése. Egyszerűen fogalmazva, ez egy olyan rendszer, ahol a névterek és az osztálynevek megfelelnek a könyvtárszerkezetnek és a fájlneveknek, azaz a `App\Core\RouterFactory` a `/path/to/App/Core/RouterFactory.php` fájlban található. Konfigurációs példa: ```js { diff --git a/best-practices/hu/dynamic-snippets.texy b/best-practices/hu/dynamic-snippets.texy index 508065649e..9234d5b6ce 100644 --- a/best-practices/hu/dynamic-snippets.texy +++ b/best-practices/hu/dynamic-snippets.texy @@ -35,7 +35,7 @@ Template: Ajaxization .[#toc-ajaxization] =============================== -Vigyük most az AJAX-ot ebbe az egyszerű alkalmazásba. Egy cikk értékelésének megváltoztatása nem elég fontos ahhoz, hogy HTTP-kérést igényeljen átirányítással, így ideális esetben ezt AJAX-szel kell elvégezni a háttérben. Használni fogjuk a [kezelőszkriptet a kiegészítésekből |https://componette.org/vojtech-dobes/nette.ajax.js/] a szokásos konvencióval, hogy az AJAX linkek CSS osztálya a `ajax`. +Vigyük most az AJAX-ot ebbe az egyszerű alkalmazásba. Egy cikk értékelésének megváltoztatása nem elég fontos ahhoz, hogy HTTP-kérést igényeljen átirányítással, így ideális esetben ezt AJAX-szel kell elvégezni a háttérben. Használni fogjuk a [kezelőszkriptet a kiegészítésekből |application:ajax#toc-naja] a szokásos konvencióval, hogy az AJAX linkek CSS osztálya a `ajax`. Azonban hogyan kell ezt konkrétan megtenni? A Nette 2 módszert kínál: a dinamikus snippet módot és a komponens módot. Mindkettőnek megvannak az előnyei és hátrányai, ezért egyenként mutatjuk be őket. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ Természetesen meg fogjuk változtatni a nézetsablont, és hozzá kell adnunk e ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/hu/form-reuse.texy b/best-practices/hu/form-reuse.texy index 9245624d97..d3ceabe3e2 100644 --- a/best-practices/hu/form-reuse.texy +++ b/best-practices/hu/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -Nagyon fontos, hogy a `FormFactory` és a `EditFormFactory` osztályok közötti kötés kompozícióval, nem pedig objektumörökléssel valósuljon meg: +Nagyon fontos, hogy a `FormFactory` és a `EditFormFactory` osztályok közötti kötés [kompozícióval |nette:introduction-to-object-oriented-programming#composition], nem pedig [objektumörökléssel |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance] valósul meg: ```php // ⛔ NO! AZ ÖRÖKSÉG NEM TARTOZIK IDE @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory Az öröklés használata ebben az esetben teljesen kontraproduktív lenne. Nagyon gyorsan problémákba ütközne. Például, ha paramétereket akarnánk hozzáadni a `create()` metódushoz; a PHP hibát jelezne, hogy a metódus aláírása eltér a szülőétől. Vagy ha a konstruktoron keresztül átadnánk egy függőséget a `EditFormFactory` osztálynak. Ez azt okozná, amit mi [konstruktor pokolnak |dependency-injection:passing-dependencies#Constructor hell] hívunk. -Általánosságban elmondható, hogy jobb a kompozíciót előnyben részesíteni az örökléssel szemben. +Általában jobb, ha a [kompozíciót |dependency-injection:faq#Why composition is preferred over inheritance] előnyben részesítjük az [örökléssel szemben |dependency-injection:faq#Why composition is preferred over inheritance]. Form kezelés .[#toc-form-handling] @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/hu/lets-create-contact-form.texy b/best-practices/hu/lets-create-contact-form.texy index 0f6e9b14d8..f13d2dc808 100644 --- a/best-practices/hu/lets-create-contact-form.texy +++ b/best-practices/hu/lets-create-contact-form.texy @@ -39,7 +39,7 @@ Mint látható, két metódust hoztunk létre. Az első metódus `createComponen De mi van akkor, ha a felhasználó nem tölt ki néhány mezőt? Ebben az esetben tudatnunk kell vele, hogy az adott mező kötelezően kitöltendő. Ezt a `setRequired()` metódussal tettük meg. Végül hozzáadtunk egy `onSuccess`[eseményt |nette:glossary#events] is, amely akkor lép működésbe, ha az űrlapot sikeresen elküldtük. A mi esetünkben meghívja a `contactFormSucceeded` metódust , amely a beküldött űrlap feldolgozásáról gondoskodik. Ezt is hozzáadjuk a kódhoz egy pillanat múlva. -Legyen a `contantForm` komponens megjelenítve a `templates/Home/default.latte` sablonban: +Legyen a `contantForm` komponens megjelenítve a `Home/default.latte` sablonban: ```latte {block content} diff --git a/best-practices/hu/microsites.texy b/best-practices/hu/microsites.texy new file mode 100644 index 0000000000..13c9024a32 --- /dev/null +++ b/best-practices/hu/microsites.texy @@ -0,0 +1,63 @@ +Hogyan írjunk mikrooldalakat +**************************** + +Képzelje el, hogy gyorsan létre kell hoznia egy kis weboldalt a cégének egy közelgő eseményéhez. Egyszerűnek, gyorsnak és felesleges bonyodalmak nélkülinek kell lennie. Azt gondolhatod, hogy egy ilyen kis projekt nem igényel robusztus keretrendszert. De mi van akkor, ha a Nette keretrendszer használatával jelentősen leegyszerűsítheti és felgyorsíthatja ezt a folyamatot? + +Még egyszerű weboldalak készítésekor sem szeretne lemondani a kényelemről. Nem akarja újra feltalálni a kereket. Legyen nyugodtan lusta és kényeztesse magát. A Nette keretrendszer mikro keretrendszerként is kiválóan használható. + +Hogyan nézhet ki egy ilyen mikrooldal? Például a weboldal teljes kódja egyetlen `index.php` fájlba helyezhető 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'); + +// létrehoz egy DI konténert a config.neon konfiguráció alapján +$configurator->addConfig(__DIR__ . '/../app/config.neon'); +$container = $configurator->createContainer(); + +// útválasztás beállítása +$router = new Nette\Application\Routers\RouteList; +$container->addService('router', $router); + +// útvonal az URL https://example.com/ URL-hez +$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { + // a böngésző nyelvének felismerése és átirányítás az URL /en vagy /de stb. címre. + $supportedLangs = ['en', 'de', 'cs']; + $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); + $presenter->redirectUrl("/$lang"); +}); + +// útvonal az URL-hez https://example.com/cs nebo https://example.com/en +$router->addRoute('', function ($presenter, string $lang) { + // a megfelelő sablon megjelenítése, például ../templates/en.latte + $template = $presenter->createTemplate() + ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); + return $template; +}); + +// futtassuk az alkalmazást! +$container->getByType(Nette\Application\Application::class)->run(); +``` + +Minden más a `/templates` szülőmappában tárolt sablonok lesznek. + +A `index.php` oldalon található PHP-kód először beállítja [a környezetet |bootstrap:], majd definiálja az [útvonalakat |application:routing#dynamic-routing-with-callbacks], végül pedig futtatja az alkalmazást. Ennek előnye, hogy a `addRoute()` függvény második paramétere lehet egy hívható, amely a megfelelő oldal megnyitásakor végrehajtódik. + + +Miért használja a Nette-et a mikrooldalakhoz? .[#toc-why-use-nette-for-microsites] +---------------------------------------------------------------------------------- + +- Azok a fejlesztők, akik valaha is kipróbálták a [Tracy-t |tracy:], ma már el sem tudják képzelni a kódolást nélküle. +- De mindenekelőtt a [Latte |latte:] templating rendszert fogja kihasználni, mert mindössze 2 oldalról akarja szétválasztani [az elrendezést és a tartalmat |latte:template-inheritance]. +- És mindenképpen az [automatikus escapingre |latte:safety-first] akarsz támaszkodni az XSS sebezhetőségek elkerülése érdekében. +- A Nette gondoskodik arról is, hogy hiba esetén soha ne jelenjenek meg PHP hibaüzenetek, hanem egy felhasználóbarát oldal jelenjen meg. +- Ha visszajelzést szeretne kapni a felhasználóktól, például kapcsolatfelvételi űrlap formájában, akkor [űrlapokat |forms:] és [adatbázist |database:] is hozzáadhat. +- A kitöltött űrlapokat is könnyen [elküldheti e-mailben |mail:]. +- Néha hasznos lehet a [gyorsítótárazás |caching:], például a feedek letöltésekor és megjelenítésekor. + +A mai korban, amikor a gyorsaság és a hatékonyság kulcsfontosságú, fontos, hogy olyan eszközökkel rendelkezzen, amelyekkel felesleges késedelmek nélkül érhet el eredményeket. A Nette keretrendszer pontosan ezt kínálja: gyors fejlesztést, biztonságot és olyan eszközök széles skáláját, mint a Tracy és a Latte, amelyek leegyszerűsítik a folyamatot. Csak telepítsen néhány Nette csomagot, és egy ilyen microsite építése gyerekjátékká válik. És tudja, hogy nincsenek rejtett biztonsági hibák. diff --git a/best-practices/hu/pagination.texy b/best-practices/hu/pagination.texy index eee3c4024b..63034de247 100644 --- a/best-practices/hu/pagination.texy +++ b/best-practices/hu/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository A Presenterben ezután befecskendezzük a modell osztályt, és a render metódusban lekérdezzük a publikált cikkeket, amelyeket átadunk a sablonhoz: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -A sablonban gondoskodunk egy cikklista rendereléséről: +A `default.latte` sablon ezután gondoskodik a cikkek felsorolásáról: ```latte {block content} @@ -114,7 +113,7 @@ A következő lépés a bemutató szerkesztése. Az aktuálisan megjelenített o A render metódust kibővítjük a Paginator példány megszerzésével, beállításával és a sablonban megjelenítendő megfelelő cikkek kiválasztásával is. A HomePresenter így fog kinézni: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ A sablon már egy oldalon belül iterálja a cikkeket, csak a lapozási linkeket ``` -Így adtunk hozzá oldalszámozást a Paginator segítségével. Ha a [Nette Database |database:explorer] [Core |database:core] [helyett a Nette Database Explorer-t |database:explorer] használjuk adatbázis-rétegként, akkor Paginator nélkül is képesek vagyunk a lapozás megvalósítására. A `Nette\Database\Table\Selection` osztály tartalmazza a [Paginatorból |api:Nette\Database\Table\Selection::_ page] átvett paginálási logikával rendelkező [page |api:Nette\Database\Table\Selection::_ page] metódust. +Így adtunk hozzá oldalszámozást a Paginator segítségével. Ha a [Nette Database |database:explorer] [Core |database:sql-way] [helyett a Nette Database Explorer-t |database:explorer] használjuk adatbázis-rétegként, akkor Paginator nélkül is képesek vagyunk a lapozás megvalósítására. A `Nette\Database\Table\Selection` osztály tartalmazza a [Paginatorból |api:Nette\Database\Table\Selection::_ page] átvett paginálási logikával rendelkező [page |api:Nette\Database\Table\Selection::_ page] metódust. A tároló így fog kinézni: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository Nem kell Paginátort létrehoznunk a Presenterben, helyette az adattár által visszaadott `Selection` objektum metódusát fogjuk használni: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/hu/post-links.texy b/best-practices/hu/post-links.texy new file mode 100644 index 0000000000..7fa8a2a16e --- /dev/null +++ b/best-practices/hu/post-links.texy @@ -0,0 +1,59 @@ +Hogyan kell helyesen használni a POST linkeket +********************************************** + +A webes alkalmazásokban, különösen az adminisztrációs felületeken alapvető szabály kell, hogy legyen, hogy a kiszolgáló állapotát megváltoztató műveleteket nem szabad a HTTP GET módszerrel végrehajtani. Ahogy a módszer neve is sugallja, a GET csak adatok lekérdezésére használható, azok megváltoztatására nem. +Az olyan műveletekhez, mint például a rekordok törlése, célszerűbb a POST módszert használni. Bár az ideális a DELETE módszer használata lenne, ez nem hívható elő JavaScript nélkül, ezért a POST módszert használják. + +Hogyan kell ezt a gyakorlatban csinálni? Használja ezt az egyszerű trükköt. A sablon elején hozzon létre egy segédűrlapot a `postForm` azonosítóval, amelyet aztán a törlés gombokhoz fog használni: + +```latte .{file:@layout.latte} +
    +``` + +Ezzel az űrlappal használhat egy `
    @@ -36,10 +38,12 @@ Moduli Comune ------ - [Come caricare il file di configurazione |bootstrap:] +- [Come scrivere micrositi |microsites] - [Perché Nette usa la notazione costante PascalCase? |https://blog.nette.org/it/per-non-urlare-nel-codice] - [Perché Nette non usa il suffisso Interface? |https://blog.nette.org/it/i-prefissi-e-i-suffissi-non-appartengono-ai-nomi-delle-interfacce] - [Suggerimenti per l'uso di Composer |composer] - [Suggerimenti su editor e strumenti |editors-and-tools] +- [Introduzione alla programmazione orientata agli oggetti |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/it/attribute-requires.texy b/best-practices/it/attribute-requires.texy new file mode 100644 index 0000000000..f6a54bf381 --- /dev/null +++ b/best-practices/it/attribute-requires.texy @@ -0,0 +1,179 @@ +Come utilizzare l'attributo `#[Requires]` Attributo +*************************************************** + +.[perex] +Quando si scrive un'applicazione web, spesso si incontra la necessità di limitare l'accesso a certe parti dell'applicazione. Forse si desidera che alcune richieste possano essere inviate solo tramite un modulo (utilizzando quindi il metodo POST) o che siano accessibili solo alle chiamate AJAX. In Nette Framework 3.2 è stato introdotto un nuovo strumento che consente di impostare tali restrizioni in modo elegante e chiaro: l'attributo `#[Requires]` attributo. + +L'attributo è un marcatore speciale in PHP, che si aggiunge prima della definizione di una classe o di un metodo. Poiché si tratta essenzialmente di una classe, è necessario includere la clausola use per far funzionare gli esempi seguenti: + +```php +use Nette\Application\Attributes\Requires; +``` + +È possibile utilizzare l'attributo `#[Requires]` con la classe del presentatore e con questi metodi: + +- `action()` +- `render()` +- `handle()` +- `createComponent()` + +Anche gli ultimi due metodi riguardano i componenti, quindi è possibile utilizzare l'attributo anche con essi. + +Se le condizioni specificate dall'attributo non sono soddisfatte, viene generato un errore HTTP 4xx. + + +Metodi HTTP .[#toc-http-methods] +-------------------------------- + +È possibile specificare quali metodi HTTP (come GET, POST, ecc.) sono consentiti per l'accesso. Ad esempio, se si desidera consentire l'accesso solo tramite l'invio di un modulo, impostare: + +```php +class AdminPresenter extends Nette\Application\UI\Presenter +{ + #[Requires(methods: 'POST')] + public function actionDelete(int $id): void + { + } +} +``` + +Perché usare POST invece di GET per le azioni di modifica dello stato e come farlo? [Leggete la guida |post-links]. + +È possibile specificare un metodo o una serie di metodi. Un caso particolare è il valore `'*'` per abilitare tutti i metodi, che i presentatori non consentono di default per [motivi di sicurezza |application:presenters#http-method-check]. + + +Chiamate AJAX .[#toc-ajax-calls] +-------------------------------- + +Se si desidera che un presentatore o un metodo sia accessibile solo per le richieste AJAX, utilizzare: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +Stessa origine .[#toc-same-origin] +---------------------------------- + +Per migliorare la sicurezza, è possibile richiedere che la richiesta venga effettuata dallo stesso dominio. In questo modo si evita la [vulnerabilità al CSRF |nette:vulnerability-protection#cross-site-request-forgery-csrf]: + +```php +#[Requires(sameOrigin: true)] +class SecurePresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Per i metodi `handle()` l'accesso dallo stesso dominio è automaticamente richiesto. Pertanto, se si desidera consentire l'accesso da qualsiasi dominio, specificare: + +```php +#[Requires(sameOrigin: false)] +public function handleList(): void +{ +} +``` + + +Accesso tramite Forward .[#toc-access-via-forward] +-------------------------------------------------- + +A volte è utile limitare l'accesso a un presentatore in modo che sia disponibile solo indirettamente, ad esempio utilizzando i metodi `forward()` o `switch()` di un altro presentatore. In questo modo si proteggono i presentatori di errori, per evitare che vengano attivati da un URL: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +In pratica, spesso è necessario contrassegnare alcune viste a cui si può accedere solo in base alla logica del presentatore. Anche in questo caso, in modo che non possano essere aperte direttamente: + +```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 + { + } +} +``` + + +Azioni specifiche .[#toc-specific-actions] +------------------------------------------ + +È anche possibile limitare l'accesso a determinati codici, come la creazione di un componente, solo per azioni specifiche nel presentatore: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +Per una singola azione, non è necessario scrivere un array: `#[Requires(actions: 'default')]` + + +Attributi personalizzati .[#toc-custom-attributes] +-------------------------------------------------- + +Se si desidera utilizzare l'attributo `#[Requires]` con le stesse impostazioni, è possibile creare un attributo personalizzato che erediterà `#[Requires]` e impostarlo secondo le proprie esigenze. + +Ad esempio, `#[SingleAction]` consente l'accesso solo attraverso l'azione `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 +{ +} +``` + +Oppure `#[RestMethods]` consentirà l'accesso tramite tutti i metodi HTTP utilizzati per l'API 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 +{ +} +``` + + +Conclusione .[#toc-conclusion] +------------------------------ + +L'attributo `#[Requires]` offre grande flessibilità e controllo sulle modalità di accesso alle pagine web. Utilizzando regole semplici ma potenti, è possibile migliorare la sicurezza e il corretto funzionamento dell'applicazione. Come si può vedere, l'uso degli attributi in Nette non solo semplifica il lavoro, ma lo rende anche sicuro. + +{{sitename: Best Practices}} diff --git a/best-practices/it/composer.texy b/best-practices/it/composer.texy index c30fc6d908..6ca7aff82e 100644 --- a/best-practices/it/composer.texy +++ b/best-practices/it/composer.texy @@ -142,6 +142,12 @@ Oppure direttamente nel file `composer.json`: ``` +Ignorare la versione di PHP .[#toc-ignoring-php-version] +======================================================== + +I pacchetti di solito specificano sia la versione più bassa di PHP con cui sono compatibili sia la versione più alta con cui sono stati testati. Se si prevede di usare una versione di PHP ancora più recente, magari a scopo di test, Composer si rifiuterà di installare tale pacchetto. La soluzione è usare l'opzione `--ignore-platform-req=php+`, che fa sì che Composer ignori i limiti superiori della versione PHP richiesta. + + Rapporti falsi .[#toc-false-reports] ==================================== @@ -183,7 +189,7 @@ Tuttavia, è anche possibile usare Composer per caricare altre classi al di fuor Successivamente, è necessario eseguire il comando `composer dumpautoload` a ogni modifica e lasciare che le tabelle di autocaricamento si rigenerino. Questo è estremamente scomodo ed è molto meglio affidare questo compito a [RobotLoader |robot-loader:], che svolge la stessa attività automaticamente in background e molto più velocemente. -La seconda opzione consiste nel seguire [PSR-4 |https://www.php-fig.org/psr/psr-4/]. In parole povere, si tratta di un sistema in cui gli spazi dei nomi e i nomi delle classi corrispondono alla struttura delle directory e ai nomi dei file, cioè `App\Router\RouterFactory` si trova nel file `/path/to/App/Router/RouterFactory.php`. Esempio di configurazione: +La seconda opzione consiste nel seguire [PSR-4 |https://www.php-fig.org/psr/psr-4/]. In parole povere, si tratta di un sistema in cui gli spazi dei nomi e i nomi delle classi corrispondono alla struttura delle directory e ai nomi dei file, cioè `App\Core\RouterFactory` si trova nel file `/path/to/App/Core/RouterFactory.php`. Esempio di configurazione: ```js { diff --git a/best-practices/it/dynamic-snippets.texy b/best-practices/it/dynamic-snippets.texy index 870ac5ebdc..c195b97853 100644 --- a/best-practices/it/dynamic-snippets.texy +++ b/best-practices/it/dynamic-snippets.texy @@ -35,7 +35,7 @@ Template: Ajaxization .[#toc-ajaxization] =============================== -Ora aggiungiamo AJAX a questa semplice applicazione. La modifica della valutazione di un articolo non è abbastanza importante da richiedere una richiesta HTTP con redirect, quindi idealmente dovrebbe essere fatta con AJAX in background. Utilizzeremo lo [script handler di add-on |https://componette.org/vojtech-dobes/nette.ajax.js/] con la solita convenzione che i link AJAX abbiano la classe CSS `ajax`. +Ora aggiungiamo AJAX a questa semplice applicazione. La modifica della valutazione di un articolo non è abbastanza importante da richiedere una richiesta HTTP con redirect, quindi idealmente dovrebbe essere fatta con AJAX in background. Utilizzeremo lo [script handler di add-on |application:ajax#toc-naja] con la solita convenzione che i link AJAX abbiano la classe CSS `ajax`. Tuttavia, come farlo in modo specifico? Nette offre due modi: quello degli snippet dinamici e quello dei componenti. Entrambi hanno pro e contro, quindi li mostreremo uno per uno. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ Naturalmente cambieremo il modello della vista e dovremo aggiungere un factory a ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/it/form-reuse.texy b/best-practices/it/form-reuse.texy index 26e0e30dbb..696ad6d9c6 100644 --- a/best-practices/it/form-reuse.texy +++ b/best-practices/it/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -È molto importante che il legame tra le classi `FormFactory` e `EditFormFactory` sia implementato tramite composizione, non tramite ereditarietà degli oggetti: +È molto importante che il legame tra le classi `FormFactory` e `EditFormFactory` sia implementato [tramite composizione |nette:introduction-to-object-oriented-programming#composition] e non tramite [ereditarietà degli oggetti |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance]: ```php // NO! L'EREDITÀ NON È QUI @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory L'uso dell'ereditarietà in questo caso sarebbe completamente controproducente. Si incorrerebbe in problemi molto rapidamente. Per esempio, se si volessero aggiungere parametri al metodo `create()`, PHP segnalerebbe un errore perché la sua firma è diversa da quella del genitore. Oppure quando si passa una dipendenza alla classe `EditFormFactory` tramite il costruttore. Questo causerebbe quello che chiamiamo l'[inferno dei costruttori |dependency-injection:passing-dependencies#Constructor hell]. -In generale, è meglio preferire la composizione all'ereditarietà. +In genere è meglio preferire la [composizione all'ereditarietà |dependency-injection:faq#Why composition is preferred over inheritance]. Gestione dei moduli .[#toc-form-handling] @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/it/lets-create-contact-form.texy b/best-practices/it/lets-create-contact-form.texy index 463ed21c39..468790dce9 100644 --- a/best-practices/it/lets-create-contact-form.texy +++ b/best-practices/it/lets-create-contact-form.texy @@ -39,7 +39,7 @@ Come si può vedere, abbiamo creato due metodi. Il primo metodo `createComponent Ma cosa succede se l'utente non compila alcuni campi? In questo caso, dovremmo fargli sapere che si tratta di un campo obbligatorio. Lo abbiamo fatto con il metodo `setRequired()`. Infine, abbiamo aggiunto anche un [evento |nette:glossary#events] `onSuccess`, che si attiva se il form viene inviato con successo. Nel nostro caso, richiama il metodo `contactFormSucceeded`, che si occupa di elaborare il modulo inviato. Lo aggiungeremo al codice tra poco. -Il componente `contantForm` deve essere reso nel template `templates/Home/default.latte`: +Il componente `contantForm` deve essere reso nel template `Home/default.latte`: ```latte {block content} diff --git a/best-practices/it/microsites.texy b/best-practices/it/microsites.texy new file mode 100644 index 0000000000..d023f24d7d --- /dev/null +++ b/best-practices/it/microsites.texy @@ -0,0 +1,63 @@ +Come scrivere micrositi +*********************** + +Immaginate di dover creare rapidamente un piccolo sito web per un evento imminente della vostra azienda. Dovrebbe essere semplice, veloce e senza inutili complicazioni. Potreste pensare che un progetto così piccolo non richieda un framework robusto. Ma se l'utilizzo del framework Nette semplificasse e velocizzasse notevolmente questo processo? + +Anche quando si creano siti web semplici, non si vuole rinunciare al comfort. Non volete reinventare la ruota. Sentitevi liberi di essere pigri e di coccolarvi. Il framework Nette può essere utilizzato in modo eccellente anche come micro framework. + +Come potrebbe essere un microsito di questo tipo? Ad esempio, l'intero codice del sito web può essere inserito in un singolo file `index.php` nella cartella pubblica: + +```php +<?php + +require __DIR__ . '/../vendor/autoload.php'; + +$configurator = new Nette\Bootstrap\Configurator; +$configurator->enableTracy(__DIR__ . '/../log'); +$configurator->setTempDirectory(__DIR__ . '/../temp'); + +// creare un contenitore DI basato sulla configurazione in config.neon +$configurator->addConfig(__DIR__ . '/../app/config.neon'); +$container = $configurator->createContainer(); + +// impostare l'instradamento +$router = new Nette\Application\Routers\RouteList; +$container->addService('router', $router); + +// rotta per l'URL https://example.com/ +$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { + // rilevare la lingua del browser e reindirizzare all'URL /en o /de ecc. + $supportedLangs = ['en', 'de', 'cs']; + $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); + $presenter->redirectUrl("/$lang"); +}); + +// rotta per l'URL https://example.com/cs nebo https://example.com/en +$router->addRoute('', function ($presenter, string $lang) { + // visualizza il modello appropriato, ad esempio ../templates/it.latte + $template = $presenter->createTemplate() + ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); + return $template; +}); + +// eseguire l'applicazione! +$container->getByType(Nette\Application\Application::class)->run(); +``` + +Tutto il resto saranno modelli memorizzati nella cartella madre `/templates`. + +Il codice PHP in `index.php` imposta prima l'[ambiente |bootstrap:], poi definisce le [rotte |application:routing#dynamic-routing-with-callbacks] e infine esegue l'applicazione. Il vantaggio è che il secondo parametro della funzione `addRoute()` può essere un callable che viene eseguito quando viene aperta la pagina corrispondente. + + +Perché usare Nette per i micrositi? .[#toc-why-use-nette-for-microsites] +------------------------------------------------------------------------ + +- Gli sviluppatori che hanno provato [Tracy |tracy:] non possono immaginare di codificare senza di esso oggi. +- Ma soprattutto, utilizzerete il sistema di template [Latte |latte:], perché da sole 2 pagine, vorrete separare [layout e contenuti |latte:template-inheritance]. +- E sicuramente vorrete affidarvi all'[escape automatico |latte:safety-first] per prevenire le vulnerabilità XSS. +- Nette garantisce inoltre che, in caso di errore, non vengano mai visualizzati messaggi di errore PHP, ma una pagina di facile consultazione. +- Se si desidera ottenere un feedback dagli utenti, ad esempio sotto forma di modulo di contatto, è possibile aggiungere [moduli |forms:] e [database |database:]. +- È anche possibile [inviare |mail:] facilmente [via e-mail |mail:] i moduli compilati. +- A volte può essere utile la [cache |caching:], ad esempio quando si scaricano e si visualizzano i feed. + +Nell'era odierna, in cui velocità ed efficienza sono fondamentali, è importante disporre di strumenti che consentano di ottenere risultati senza inutili ritardi. Il framework Nette offre proprio questo: sviluppo veloce, sicurezza e un'ampia gamma di strumenti come Tracy e Latte che semplificano il processo. Basta installare alcuni pacchetti Nette e la costruzione di un microsito di questo tipo diventa un gioco da ragazzi. E si sa che non ci sono falle di sicurezza nascoste. diff --git a/best-practices/it/pagination.texy b/best-practices/it/pagination.texy index f88acbacb5..b82d4deb7b 100644 --- a/best-practices/it/pagination.texy +++ b/best-practices/it/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository Nel Presenter iniettiamo poi la classe Model e nel metodo render chiediamo gli articoli pubblicati che passiamo al template: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -Nel modello, ci occuperemo di rendere un elenco di articoli: +Il modello `default.latte` si occuperà di elencare gli articoli: ```latte {block content} @@ -114,7 +113,7 @@ Il passo successivo è modificare il presentatore. Inoltreremo il numero della p Espandiamo inoltre il metodo render per ottenere l'istanza di Paginator, impostandola e selezionando gli articoli corretti da visualizzare nel template. HomePresenter avrà questo aspetto: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ Il template itera già gli articoli in una pagina, basta aggiungere i link di pa ``` -Ecco come abbiamo aggiunto la paginazione utilizzando Paginator. Se si utilizza [Nette Database Explorer |database:explorer] al posto di [Nette Database Core |database:core] come livello di database, siamo in grado di implementare la paginazione anche senza Paginator. La classe `Nette\Database\Table\Selection` contiene il metodo [page |api:Nette\Database\Table\Selection::_ page] con la logica di paginazione presa da Paginator. +Ecco come abbiamo aggiunto la paginazione utilizzando Paginator. Se si utilizza [Nette Database Explorer |database:explorer] al posto di [Nette Database Core |database:sql-way] come livello di database, siamo in grado di implementare la paginazione anche senza Paginator. La classe `Nette\Database\Table\Selection` contiene il metodo [page |api:Nette\Database\Table\Selection::_ page] con la logica di paginazione presa da Paginator. Il repository avrà questo aspetto: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository Non è necessario creare il Paginator nel Presenter, ma si utilizzerà il metodo dell'oggetto `Selection` restituito dal repository: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/it/post-links.texy b/best-practices/it/post-links.texy new file mode 100644 index 0000000000..8fbca0e2c4 --- /dev/null +++ b/best-practices/it/post-links.texy @@ -0,0 +1,59 @@ +Come utilizzare correttamente i link POST +***************************************** + +Nelle applicazioni web, soprattutto nelle interfacce amministrative, dovrebbe essere una regola di base che le azioni che modificano lo stato del server non dovrebbero essere eseguite tramite il metodo HTTP GET. Come suggerisce il nome del metodo, GET deve essere usato solo per recuperare dati, non per modificarli. +Per azioni come la cancellazione di record, è più appropriato usare il metodo POST. Anche se l'ideale sarebbe usare il metodo DELETE, questo non può essere invocato senza JavaScript, quindi storicamente si usa POST. + +Come fare nella pratica? Utilizzando questo semplice trucco. All'inizio del modello, creare un modulo di aiuto con l'identificatore `postForm`, che verrà poi utilizzato per i pulsanti di cancellazione: + +```latte .{file:@layout.latte} +
    +``` + +Con questo modulo, è possibile utilizzare un elemento `
    @@ -36,10 +38,12 @@ Formularze Wspólne ------- - [Jak załadować plik konfiguracyjny |bootstrap:] +- [Jak pisać mikrowitryny |microsites] - [Dlaczego Nette używa notacji stałej PascalCase? |https://blog.nette.org/pl/aby-mniej-krzyczec-w-kodzie] - [Dlaczego Nette nie używa przyrostka Interface? |https://blog.nette.org/pl/przedrostki-i-przyrostki-nie-sa-czescia-nazw-interfejsow] - [Kompozytor: wskazówki dotyczące użytkowania |composer] - [Porady dotyczące edytorów i narzędzi |editors-and-tools] +- [Wprowadzenie do programowania obiektowego |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/pl/attribute-requires.texy b/best-practices/pl/attribute-requires.texy new file mode 100644 index 0000000000..2c79b3fc87 --- /dev/null +++ b/best-practices/pl/attribute-requires.texy @@ -0,0 +1,179 @@ +Jak używać atrybutu `#[Requires]` Atrybut +***************************************** + +.[perex] +Podczas pisania aplikacji internetowej często pojawia się potrzeba ograniczenia dostępu do niektórych jej części. Być może chcesz, aby niektóre żądania mogły wysyłać dane tylko za pośrednictwem formularza (a więc przy użyciu metody POST) lub aby były dostępne tylko dla wywołań AJAX. W Nette Framework 3.2 wprowadzono nowe narzędzie, które pozwala ustawić takie ograniczenia w elegancki i przejrzysty sposób: atrybut `#[Requires]` atrybut. + +Atrybut jest specjalnym znacznikiem w PHP, który dodaje się przed definicją klasy lub metody. Ponieważ jest to zasadniczo klasa, musisz dołączyć klauzulę use, aby poniższe przykłady działały: + +```php +use Nette\Application\Attributes\Requires; +``` + +Atrybutu `#[Requires]` z samą klasą prezentera i tymi metodami: + +- `action()` +- `render()` +- `handle()` +- `createComponent()` + +Dwie ostatnie metody również dotyczą komponentów, więc można użyć atrybutu również z nimi. + +Jeśli warunki określone przez atrybut nie są spełnione, wywoływany jest błąd HTTP 4xx. + + +Metody HTTP .[#toc-http-methods] +-------------------------------- + +Można określić, które metody HTTP (takie jak GET, POST itp.) są dozwolone dla dostępu. Na przykład, jeśli chcesz zezwolić na dostęp tylko poprzez przesłanie formularza, ustaw: + +```php +class AdminPresenter extends Nette\Application\UI\Presenter +{ + #[Requires(methods: 'POST')] + public function actionDelete(int $id): void + { + } +} +``` + +Dlaczego należy używać POST zamiast GET do akcji zmieniających stan i jak to zrobić? [Przeczytaj przewodnik |post-links]. + +Można określić metodę lub tablicę metod. Szczególnym przypadkiem jest wartość `'*'`, która włącza wszystkie metody, na które prezentery nie pozwalają domyślnie ze [względów |application:presenters#http-method-check] bezpieczeństwa. + + +Wywołania AJAX .[#toc-ajax-calls] +--------------------------------- + +Jeśli chcesz, aby prezenter lub metoda były dostępne tylko dla żądań AJAX, użyj: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +To samo pochodzenie .[#toc-same-origin] +--------------------------------------- + +Aby zwiększyć bezpieczeństwo, można wymagać, aby żądanie zostało wykonane z tej samej domeny. Zapobiega to [podatności na CSRF |nette:vulnerability-protection#cross-site-request-forgery-csrf]: + +```php +#[Requires(sameOrigin: true)] +class SecurePresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Dla `handle()` automatycznie wymagany jest dostęp z tej samej domeny. Jeśli więc chcesz zezwolić na dostęp z dowolnej domeny, określ: + +```php +#[Requires(sameOrigin: false)] +public function handleList(): void +{ +} +``` + + +Dostęp przez Forward .[#toc-access-via-forward] +----------------------------------------------- + +Czasami przydatne jest ograniczenie dostępu do prezentera, tak aby był on dostępny tylko pośrednio, na przykład przy użyciu metod `forward()` lub `switch()` z innego prezentera. W ten sposób chronione są prezentery błędów, aby uniemożliwić ich wywołanie z adresu URL: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +W praktyce często konieczne jest oznaczenie pewnych widoków, do których można uzyskać dostęp tylko w oparciu o logikę w prezenterze. Ponownie, aby nie można było ich otworzyć bezpośrednio: + +```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 działania .[#toc-specific-actions] +-------------------------------------------- + +Można również ograniczyć dostęp do określonego kodu, takiego jak tworzenie komponentu, tylko dla określonych akcji w prezenterze: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +Dla pojedynczej akcji nie ma potrzeby pisania tablicy: `#[Requires(actions: 'default')]` + + +Atrybuty niestandardowe .[#toc-custom-attributes] +------------------------------------------------- + +Jeśli chcesz używać atrybutu `#[Requires]` z tymi samymi ustawieniami, można utworzyć własny atrybut, który będzie dziedziczył `#[Requires]` i ustawić go zgodnie z własnymi potrzebami. + +Na przykład, `#[SingleAction]` zezwala na dostęp tylko poprzez akcję `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 +{ +} +``` + +lub `#[RestMethods]` umożliwi dostęp za pośrednictwem wszystkich metod HTTP używanych w interfejsie API 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 +{ +} +``` + + +Wnioski .[#toc-conclusion] +-------------------------- + +Atrybut `#[Requires]` zapewnia dużą elastyczność i kontrolę nad sposobem dostępu do stron internetowych. Korzystając z prostych, ale potężnych reguł, można zwiększyć bezpieczeństwo i prawidłowe funkcjonowanie aplikacji. Jak widać, korzystanie z atrybutów w Nette może nie tylko uprościć pracę, ale także ją zabezpieczyć. + +{{sitename: Best Practices}} diff --git a/best-practices/pl/composer.texy b/best-practices/pl/composer.texy index f208932801..77950657da 100644 --- a/best-practices/pl/composer.texy +++ b/best-practices/pl/composer.texy @@ -142,6 +142,12 @@ Lub bezpośrednio w pliku `composer.json`: ``` +Ignorowanie wersji PHP .[#toc-ignoring-php-version] +=================================================== + +Pakiety zazwyczaj określają zarówno najniższą wersję PHP, z którą są kompatybilne, jak i najwyższą wersję, z którą zostały przetestowane. Jeśli planujesz użyć jeszcze nowszej wersji PHP, być może w celach testowych, Composer odmówi instalacji takiego pakietu. Rozwiązaniem jest użycie opcji `--ignore-platform-req=php+`, która powoduje, że Composer ignoruje górne limity wymaganej wersji PHP. + + Fałszywe raporty .[#toc-false-reports] ====================================== @@ -183,7 +189,7 @@ Jednakże możliwe jest również użycie Composera do załadowania innych klas Następnie musisz uruchomić polecenie `composer dumpautoload` za każdym razem, gdy dokonujesz zmiany i masz ponownie wygenerowane tabele autoloader. Jest to niezwykle uciążliwe i zdecydowanie lepiej powierzyć to zadanie [RobotLoaderowi |robot-loader:], który wykonuje tę samą pracę automatycznie w tle i znacznie szybciej. -Inną możliwością jest zastosowanie się do [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Mówiąc najprościej, jest to system, w którym przestrzenie nazw i nazwy klas odpowiadają strukturom katalogów i nazwom plików, więc na przykład `App\Router\RouterFactory` będzie w pliku `/path/to/App/Router/RouterFactory.php`. Przykładowa konfiguracja: +Inną możliwością jest zastosowanie się do [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Mówiąc najprościej, jest to system, w którym przestrzenie nazw i nazwy klas odpowiadają strukturom katalogów i nazwom plików, więc na przykład `App\Core\RouterFactory` będzie w pliku `/path/to/App/Core/RouterFactory.php`. Przykładowa konfiguracja: ```js { diff --git a/best-practices/pl/dynamic-snippets.texy b/best-practices/pl/dynamic-snippets.texy index c1ad93724d..74c1c17c1f 100644 --- a/best-practices/pl/dynamic-snippets.texy +++ b/best-practices/pl/dynamic-snippets.texy @@ -35,7 +35,7 @@ Szablon: Ajaxizacja .[#toc-ajaxization] ============================== -Wyposażmy teraz tę prostą aplikację w AJAX. Zmiana oceny artykułu nie jest na tyle ważna, aby wymagała przekierowania, więc idealnie powinna być wykonana z AJAX w tle. Wykorzystamy [skrypt handler z dodatków ze |https://componette.org/vojtech-dobes/nette.ajax.js/] zwykłą konwencją, że linki AJAX mają klasę CSS `ajax`. +Wyposażmy teraz tę prostą aplikację w AJAX. Zmiana oceny artykułu nie jest na tyle ważna, aby wymagała przekierowania, więc idealnie powinna być wykonana z AJAX w tle. Wykorzystamy [skrypt handler z dodatków ze |application:ajax#toc-naja] zwykłą konwencją, że linki AJAX mają klasę CSS `ajax`. Jednak jak to konkretnie zrobić? Nette oferuje 2 ścieżki: tzw. ścieżkę dynamicznych snippetów oraz ścieżkę komponentów. Oba mają swoje plusy i minusy, więc pokażemy je po kolei. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ Oczywiście zmienimy szablon widoku i będziemy musieli dodać fabrykę do preze ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/pl/form-reuse.texy b/best-practices/pl/form-reuse.texy index 46ee665dec..735904cffb 100644 --- a/best-practices/pl/form-reuse.texy +++ b/best-practices/pl/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -Bardzo ważne jest, że wiązanie między klasami `FormFactory` i `EditFormFactory` jest realizowane przez kompozycję, a nie dziedziczenie obiektów: +Bardzo ważne jest, aby powiązanie między klasami `FormFactory` i `EditFormFactory` było realizowane [przez kompozycję |nette:introduction-to-object-oriented-programming#composition], a nie [dziedziczenie obiektów |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance]: ```php // ⛔ NIE! DZIEDZICZENIE NIE NALEŻY DO TEGO MIEJSCA @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory Używanie dziedziczenia w tym przypadku byłoby całkowicie przeciwne do zamierzonego. Bardzo szybko napotkałbyś problemy. Na przykład, gdybyś chciał dodać parametry do metody `create()`; PHP zgłosiłoby błąd, że jej podpis jest inny niż rodzica. Albo podczas przekazywania zależności do klasy `EditFormFactory` poprzez konstruktor. To spowodowałoby coś, co nazywamy [piekłem konstru |dependency-injection:passing-dependencies#Constructor hell]ktora. -Ogólnie rzecz biorąc, lepiej jest preferować kompozycję niż dziedziczenie. +Ogólnie rzecz biorąc, lepiej jest preferować [kompozycję niż dziedziczenie |dependency-injection:faq#Why composition is preferred over inheritance]. Obsługa formularzy .[#toc-form-handling] @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/pl/lets-create-contact-form.texy b/best-practices/pl/lets-create-contact-form.texy index 25c109b3c9..40f832e01b 100644 --- a/best-practices/pl/lets-create-contact-form.texy +++ b/best-practices/pl/lets-create-contact-form.texy @@ -39,7 +39,7 @@ Jak widać, stworzyliśmy dwie metody. Pierwsza metoda `createComponentContactFo Ale co jeśli użytkownik nie wypełni niektórych pól? W takim przypadku powinniśmy dać mu znać, że jest to pole wymagane. Zrobiliśmy to za pomocą metody `setRequired()`. Na koniec dodaliśmy również [zdarzenie |nette:glossary#events] `onSuccess`, które jest wywoływane, jeśli formularz zostanie przesłany pomyślnie. W naszym przypadku wywołuje ono metodę `contactFormSucceeded`, która zajmuje się przetwarzaniem przesłanego formularza. Za chwilę dodamy to do kodu. -Niech komponent `contantForm` będzie renderowany w szablonie `templates/Home/default.latte`: +Niech komponent `contantForm` będzie renderowany w szablonie `Home/default.latte`: ```latte {block content} diff --git a/best-practices/pl/microsites.texy b/best-practices/pl/microsites.texy new file mode 100644 index 0000000000..64a7bfcc71 --- /dev/null +++ b/best-practices/pl/microsites.texy @@ -0,0 +1,63 @@ +Jak pisać mikrostrony internetowe +********************************* + +Wyobraź sobie, że musisz szybko stworzyć małą stronę internetową na nadchodzące wydarzenie Twojej firmy. Powinna być prosta, szybka i bez zbędnych komplikacji. Można by pomyśleć, że tak mały projekt nie wymaga solidnego frameworka. Ale co, jeśli użycie frameworka Nette może znacznie uprościć i przyspieszyć ten proces? + +Nawet tworząc proste strony internetowe, nie chcesz rezygnować z wygody. Nie chcesz wymyślać koła na nowo. Możesz być leniwy i rozpieszczać samego siebie. Nette Framework może być również doskonale wykorzystywany jako mikro framework. + +Jak może wyglądać taka mikrostrona? Na przykład, cały kod strony może być umieszczony w pojedynczym pliku `index.php` w folderze publicznym: + +```php +<?php + +require __DIR__ . '/../vendor/autoload.php'; + +$configurator = new Nette\Bootstrap\Configurator; +$configurator->enableTracy(__DIR__ . '/../log'); +$configurator->setTempDirectory(__DIR__ . '/../temp'); + +// utworzenie kontenera DI na podstawie konfiguracji w config.neon +$configurator->addConfig(__DIR__ . '/../app/config.neon'); +$container = $configurator->createContainer(); + +// skonfigurować routing +$router = new Nette\Application\Routers\RouteList; +$container->addService('router', $router); + +// trasa dla adresu URL https://example.com/ +$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { + // wykrywanie języka przeglądarki i przekierowanie na URL /en lub /de itp. + $supportedLangs = ['en', 'de', 'cs']; + $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); + $presenter->redirectUrl("/$lang"); +}); + +// trasa dla adresu URL https://example.com/cs nebo https://example.com/en +$router->addRoute('', function ($presenter, string $lang) { + // wyświetla odpowiedni szablon, na przykład ../templates/en.latte + $template = $presenter->createTemplate() + ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); + return $template; +}); + +// uruchom aplikację! +$container->getByType(Nette\Application\Application::class)->run(); +``` + +Cała reszta będzie szablonami przechowywanymi w folderze nadrzędnym `/templates`. + +Kod PHP w `index.php` najpierw konfiguruje [środowisko |bootstrap:], następnie definiuje [trasy |application:routing#dynamic-routing-with-callbacks], a na końcu uruchamia aplikację. Zaletą jest to, że drugim parametrem funkcji `addRoute()` może być wywołanie, które jest wykonywane po otwarciu odpowiedniej strony. + + +Dlaczego warto używać Nette dla mikrostron? .[#toc-why-use-nette-for-microsites] +-------------------------------------------------------------------------------- + +- Programiści, którzy kiedykolwiek wypróbowali [Tracy |tracy:], nie wyobrażają sobie dziś kodowania bez niego. +- Ale przede wszystkim będziesz korzystać z systemu szablonów [Latte |latte:], ponieważ z zaledwie 2 stron będziesz chciał oddzielić [układ i zawartość |latte:template-inheritance]. +- I zdecydowanie chcesz polegać na [automatycznym uciekaniu |latte:safety-first], aby zapobiec lukom w zabezpieczeniach XSS. +- Nette zapewnia również, że w przypadku błędu komunikaty o błędach PHP nigdy nie będą wyświetlane, ale zamiast tego pojawi się przyjazna dla użytkownika strona. +- Jeśli chcesz uzyskać informacje zwrotne od użytkowników, na przykład w formie formularza kontaktowego, możesz również dodać [formularze |forms:] i [bazę danych |database:]. +- Możesz także łatwo wysyłać wypełnione formularze [pocztą e-mail |mail:]. +- Czasami przydatne może okazać się [buforowanie |caching:], na przykład podczas pobierania i wyświetlania kanałów. + +W dzisiejszych czasach, gdzie szybkość i wydajność są kluczowe, ważne jest, aby mieć narzędzia, które pozwalają osiągnąć wyniki bez zbędnych opóźnień. Framework Nette oferuje właśnie to - szybki rozwój, bezpieczeństwo i szeroką gamę narzędzi, takich jak Tracy i Latte, które upraszczają proces. Wystarczy zainstalować kilka pakietów Nette, a zbudowanie takiej mikrostrony staje się dziecinnie proste. I wiesz, że nie ma żadnych ukrytych luk w zabezpieczeniach. diff --git a/best-practices/pl/pagination.texy b/best-practices/pl/pagination.texy index 83ff2c4cd9..bf7c5a7cb0 100644 --- a/best-practices/pl/pagination.texy +++ b/best-practices/pl/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository Następnie wstrzykujemy klasę modelu w prezenterze i w metodzie render żądamy opublikowanych artykułów do przekazania do szablonu: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -W szablonie zajmujemy się następnie wyszczególnieniem artykułów: +Szablon `default.latte` zajmie się następnie listą artykułów: ```latte {block content} @@ -114,7 +113,7 @@ Następnie zabierzemy się do pracy nad modyfikacją prezentera. Do metody rende Następnie rozszerzymy również metodę render, aby uzyskać instancję Paginatora, skonfigurować ją i wybrać odpowiednie artykuły do wyświetlenia w szablonie. HomePresenter po modyfikacjach będzie wyglądał tak: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ Szablon już iteruje po tylko artykułach jednej strony, musimy tylko dodać lin ``` -W ten sposób dodaliśmy do strony paginację Paginator. W przypadku, gdy jako warstwy bazy danych użyjemy [Nette |database:core] Database [Explorer |database:explorer] zamiast [Nette Database Core |database:core], jesteśmy w stanie zaimplementować paginację bez użycia Paginatora. Klasa `Nette\Database\Table\Selection` zawiera metodę [page |api:Nette\Database\Table\Selection::_page] z logiką paginacji zaczerpniętą z Paginatora. +W ten sposób dodaliśmy do strony paginację Paginator. W przypadku, gdy jako warstwy bazy danych użyjemy [Nette |database:sql-way] Database [Explorer |database:explorer] zamiast [Nette Database Core |database:sql-way], jesteśmy w stanie zaimplementować paginację bez użycia Paginatora. Klasa `Nette\Database\Table\Selection` zawiera metodę [page |api:Nette\Database\Table\Selection::_page] z logiką paginacji zaczerpniętą z Paginatora. Repozytorium będzie wyglądać tak z tą implementacją: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository Nie musimy tworzyć Paginatora w prezenterze, zamiast tego używamy metody klasy `Selection` zwracanej przez repozytorium: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/pl/post-links.texy b/best-practices/pl/post-links.texy new file mode 100644 index 0000000000..9db658ec70 --- /dev/null +++ b/best-practices/pl/post-links.texy @@ -0,0 +1,59 @@ +Jak prawidłowo używać linków POST +********************************* + +W aplikacjach internetowych, zwłaszcza w interfejsach administracyjnych, podstawową zasadą powinno być to, że działania zmieniające stan serwera nie powinny być wykonywane za pomocą metody HTTP GET. Jak sugeruje nazwa metody, GET powinien być używany tylko do pobierania danych, a nie do ich zmiany. +W przypadku działań takich jak usuwanie rekordów, bardziej odpowiednie jest użycie metody POST. Chociaż idealnym rozwiązaniem byłoby użycie metody DELETE, nie można jej wywołać bez JavaScript, dlatego POST jest historycznie używany. + +Jak to zrobić w praktyce? Użyj tej prostej sztuczki. Na początku szablonu utwórz formularz pomocniczy o identyfikatorze `postForm`, który następnie użyjesz dla przycisków usuwania: + +```latte .{file:@layout.latte} +
    +``` + +W tym formularzu możesz użyć `
    @@ -36,10 +38,12 @@ Formulários Comum ----- - [Como carregar o arquivo de configuração |bootstrap:] +- [Como escrever microsites |microsites] - [Por que a Nette usa a notação constante PascalCase? |https://blog.nette.org/pt/por-menos-gritos-no-codigo] - [Por que Nette não usa o sufixo Interface? |https://blog.nette.org/pt/prefixos-e-sufixos-nao-pertencem-a-nomes-de-interface] - [Dicas de uso do compositor |composer] - [Dicas sobre editores e ferramentas |editors-and-tools] +- [Introdução à programação orientada a objetos |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/pt/attribute-requires.texy b/best-practices/pt/attribute-requires.texy new file mode 100644 index 0000000000..af9f1f4ec0 --- /dev/null +++ b/best-practices/pt/attribute-requires.texy @@ -0,0 +1,179 @@ +Como usar o `#[Requires]` Atributo +********************************** + +.[perex] +Ao escrever um aplicativo da Web, você frequentemente se depara com a necessidade de restringir o acesso a determinadas partes do aplicativo. Talvez você queira que algumas solicitações só possam enviar dados por meio de um formulário (usando, portanto, o método POST) ou que sejam acessíveis somente a chamadas AJAX. No Nette Framework 3.2, foi introduzida uma nova ferramenta que permite que você defina essas restrições de forma elegante e clara: o atributo `#[Requires]` atributo. + +O atributo é um marcador especial no PHP, que você adiciona antes da definição de uma classe ou método. Como ele é essencialmente uma classe, você precisa incluir a cláusula use para que os exemplos a seguir funcionem: + +```php +use Nette\Application\Attributes\Requires; +``` + +Você pode usar o atributo `#[Requires]` com a própria classe do apresentador e nesses métodos: + +- `action()` +- `render()` +- `handle()` +- `createComponent()` + +Os dois últimos métodos também se referem a componentes, portanto, você também pode usar o atributo com eles. + +Se as condições especificadas pelo atributo não forem atendidas, será acionado um erro HTTP 4xx. + + +Métodos HTTP .[#toc-http-methods] +--------------------------------- + +Você pode especificar quais métodos HTTP (como GET, POST, etc.) são permitidos para acesso. Por exemplo, se você quiser permitir o acesso somente por meio do envio de 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 de alteração de estado e como fazer isso? [Leia o guia |post-links]. + +Você pode especificar um método ou uma matriz de métodos. Um caso especial é o valor `'*'` para ativar todos os métodos, que os apresentadores não permitem por padrão por [motivos de segurança |application:presenters#http-method-check]. + + +Chamadas AJAX .[#toc-ajax-calls] +-------------------------------- + +Se você quiser que um apresentador ou método seja acessível somente para solicitações AJAX, use: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +Mesma origem .[#toc-same-origin] +-------------------------------- + +Para aumentar a segurança, você pode exigir que a solicitação seja feita a partir do mesmo domínio. Isso evita a [vulnerabilidade ao 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()` o acesso do mesmo domínio é automaticamente necessário. Portanto, se você quiser permitir o acesso de qualquer domínio, especifique: + +```php +#[Requires(sameOrigin: false)] +public function handleList(): void +{ +} +``` + + +Acesso via Forward .[#toc-access-via-forward] +--------------------------------------------- + +Às vezes, é útil restringir o acesso a um apresentador para que ele esteja disponível apenas indiretamente, por exemplo, usando os métodos `forward()` ou `switch()` de outro apresentador. É assim que os apresentadores de erros são protegidos para evitar que sejam acionados a partir de um URL: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Na prática, muitas vezes é necessário marcar determinadas exibições que só podem ser acessadas com base na lógica do apresentador. Novamente, para que elas 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 .[#toc-specific-actions] +------------------------------------------ + +Você também pode restringir o acesso a determinados códigos, como a criação de um componente, apenas para ações específicas no apresentador: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +Para uma única ação, não há necessidade de escrever uma matriz: `#[Requires(actions: 'default')]` + + +Atributos personalizados .[#toc-custom-attributes] +-------------------------------------------------- + +Se você quiser usar o atributo `#[Requires]` repetidamente com as mesmas configurações, você pode criar seu próprio atributo que herdará o atributo `#[Requires]` e defini-lo de acordo com suas necessidades. + +Por exemplo, `#[SingleAction]` permite o acesso somente por meio 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 por meio de todos os métodos HTTP usados para a API 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 .[#toc-conclusion] +---------------------------- + +O atributo `#[Requires]` oferece grande flexibilidade e controle sobre como as páginas da Web são acessadas. Usando regras simples, porém poderosas, você pode aumentar a segurança e o funcionamento adequado do seu aplicativo. Como você pode ver, o uso de atributos no Nette pode não apenas simplificar seu trabalho, mas também torná-lo seguro. + +{{sitename: Best Practices}} diff --git a/best-practices/pt/composer.texy b/best-practices/pt/composer.texy index d6f7112628..bc48d771b3 100644 --- a/best-practices/pt/composer.texy +++ b/best-practices/pt/composer.texy @@ -142,6 +142,12 @@ Ou diretamente no arquivo `composer.json': ``` +Ignorando a versão do PHP .[#toc-ignoring-php-version] +====================================================== + +Normalmente, os pacotes especificam a versão mais baixa do PHP com a qual são compatíveis e a versão mais alta com a qual foram testados. Se você planeja usar uma versão ainda mais recente do PHP, talvez para fins de teste, o Composer se recusará a instalar esse pacote. A solução é usar a opção `--ignore-platform-req=php+`, que faz com que o Composer ignore os limites superiores da versão do PHP necessária. + + Falsos relatórios .[#toc-false-reports] ======================================= @@ -183,7 +189,7 @@ Entretanto, também é possível utilizar o Composer para carregar outras classe Em seguida, é necessário executar o comando `composer dumpautoload` a cada mudança e deixar as mesas de auto-carga se regenerar. Isto é extremamente inconveniente, e é muito melhor confiar esta tarefa ao [RobotLoader |robot-loader:], que executa a mesma atividade automaticamente em segundo plano e muito mais rápido. -A segunda opção é seguir o [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Simplesmente dizendo, é um sistema onde os namespaces e nomes de classes correspondem à estrutura do diretório e nomes de arquivos, ou seja, `App\Router\RouterFactory` está localizado no arquivo `/path/to/App/Router/RouterFactory.php`. Exemplo de configuração: +A segunda opção é seguir o [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Simplesmente dizendo, é um sistema onde os namespaces e nomes de classes correspondem à estrutura do diretório e nomes de arquivos, ou seja, `App\Core\RouterFactory` está localizado no arquivo `/path/to/App/Core/RouterFactory.php`. Exemplo de configuração: ```js { diff --git a/best-practices/pt/dynamic-snippets.texy b/best-practices/pt/dynamic-snippets.texy index 660329a182..65434b41c0 100644 --- a/best-practices/pt/dynamic-snippets.texy +++ b/best-practices/pt/dynamic-snippets.texy @@ -35,7 +35,7 @@ Modelo: Ajaxização .[#toc-ajaxization] ============================== -Vamos agora trazer o AJAX para esta simples aplicação. Mudar a classificação de um artigo não é suficientemente importante para exigir um pedido HTTP com redirecionamento, então o ideal é que isso seja feito com AJAX em segundo plano. Usaremos o [script do handler dos add-ons |https://componette.org/vojtech-dobes/nette.ajax.js/] com a convenção usual de que os links AJAX têm a classe CSS `ajax`. +Vamos agora trazer o AJAX para esta simples aplicação. Mudar a classificação de um artigo não é suficientemente importante para exigir um pedido HTTP com redirecionamento, então o ideal é que isso seja feito com AJAX em segundo plano. Usaremos o [script do handler dos add-ons |application:ajax#toc-naja] com a convenção usual de que os links AJAX têm a classe CSS `ajax`. No entanto, como fazer isso especificamente? A Nette oferece 2 maneiras: a maneira dinâmica do snippet e a maneira dos componentes. Ambas têm seus prós e contras, por isso vamos mostrar-lhes uma a uma. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ Modelo de componente: ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/pt/form-reuse.texy b/best-practices/pt/form-reuse.texy index 90da87b3c5..7555fad062 100644 --- a/best-practices/pt/form-reuse.texy +++ b/best-practices/pt/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -É muito importante que a ligação entre as classes `FormFactory` e `EditFormFactory` seja implementada por composição e não por herança de objetos: +É muito importante que a vinculação entre as classes `FormFactory` e `EditFormFactory` seja implementada [por composição |nette:introduction-to-object-oriented-programming#composition], e não por [herança de objetos |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance]: ```php // ⛔ NÃO! A HERANÇA NÃO PERTENCE AQUI @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory Usar a herança neste caso seria completamente contraproducente. Você se depararia com problemas muito rapidamente. Por exemplo, se você quisesse adicionar parâmetros ao método `create()`; o PHP relataria um erro de que sua assinatura era diferente da dos pais. Ou ao passar uma dependência para a classe `EditFormFactory` através do construtor. Isto causaria o que chamamos de [inferno do construtor |dependency-injection:passing-dependencies#Constructor hell]. -Em geral, é melhor preferir a composição do que a herança. +Em geral, é melhor preferir a [composição à herança |dependency-injection:faq#Why composition is preferred over inheritance]. Manuseio de formulários .[#toc-form-handling] @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/pt/lets-create-contact-form.texy b/best-practices/pt/lets-create-contact-form.texy index 73db878db9..6fd619dd72 100644 --- a/best-practices/pt/lets-create-contact-form.texy +++ b/best-practices/pt/lets-create-contact-form.texy @@ -39,7 +39,7 @@ Como você pode ver, nós criamos dois métodos. O primeiro método `createCompo Mas e se o usuário não preencher alguns campos? Nesse caso, devemos avisá-lo que se trata de um campo obrigatório. Fizemos isso com o método `setRequired()`. Finalmente, adicionamos também um [evento |nette:glossary#events] `onSuccess`, que é acionado se o formulário for submetido com sucesso. Em nosso caso, ele chama o método `contactFormSucceeded`, que se encarrega de processar o formulário submetido. Acrescentaremos isso ao código em um momento. -Deixe o componente `contantForm` ser apresentado no modelo `templates/Home/default.latte`: +Deixe o componente `contantForm` ser apresentado no modelo `Home/default.latte`: ```latte {block content} diff --git a/best-practices/pt/microsites.texy b/best-practices/pt/microsites.texy new file mode 100644 index 0000000000..d436664b88 --- /dev/null +++ b/best-practices/pt/microsites.texy @@ -0,0 +1,63 @@ +Como escrever microsites +************************ + +Imagine que você precise criar rapidamente um pequeno site para um evento futuro da sua empresa. Ele deve ser simples, rápido e sem complicações desnecessárias. Você pode pensar que um projeto tão pequeno não requer uma estrutura robusta. Mas e se o uso da estrutura Nette pudesse simplificar e acelerar significativamente esse processo? + +Mesmo ao criar sites simples, você não quer abrir mão do conforto. Você não quer reinventar a roda. Sinta-se à vontade para ser preguiçoso e se mimar. O Nette Framework também pode ser usado de forma excelente como uma microestrutura. + +Como seria esse microsite? Por exemplo, todo o código do site pode ser colocado 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'); + +// Crie um contêiner DI com base na configuração em config.neon +$configurator->addConfig(__DIR__ . '/../app/config.neon'); +$container = $configurator->createContainer(); + +// configurar o roteamento +$router = new Nette\Application\Routers\RouteList; +$container->addService('router', $router); + +// rota para o URL https://example.com/ +$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { + // detectar o idioma do navegador e redirecionar para o URL /en ou /de etc. + $supportedLangs = ['en', 'de', 'cs']; + $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); + $presenter->redirectUrl("/$lang"); +}); + +// rota para o URL https://example.com/cs nebo https://example.com/en +$router->addRoute('', function ($presenter, string $lang) { + // exibir o modelo apropriado, por exemplo, ../templates/en.latte + $template = $presenter->createTemplate() + ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); + return $template; +}); + +// Execute o aplicativo! +$container->getByType(Nette\Application\Application::class)->run(); +``` + +Todo o resto serão modelos armazenados na pasta principal `/templates`. + +O código PHP em `index.php` primeiro [configura o ambiente |bootstrap:], depois define [as rotas |application:routing#dynamic-routing-with-callbacks] e, por fim, executa o aplicativo. A vantagem é que o segundo parâmetro da função `addRoute()` pode ser um chamável que é executado quando a página correspondente é aberta. + + +Por que usar o Nette para microsites? .[#toc-why-use-nette-for-microsites] +-------------------------------------------------------------------------- + +- Os desenvolvedores que já experimentaram [o Tracy |tracy:] não conseguem imaginar a codificação sem ele hoje. +- Mas, acima de tudo, você utilizará o sistema de modelos [Latte |latte:], pois a partir de apenas duas páginas, você desejará separar [o layout do conteúdo |latte:template-inheritance]. +- E, com certeza, você vai querer contar com o [escape automático |latte:safety-first] para evitar vulnerabilidades de XSS. +- O Nette também garante que, em caso de erro, as mensagens de erro do PHP nunca serão exibidas, mas, em vez disso, será exibida uma página de fácil utilização. +- Se quiser obter feedback dos usuários, por exemplo, na forma de um formulário de contato, também é possível adicionar [formulários |forms:] e [banco de dados |database:]. +- Os formulários preenchidos também podem ser facilmente [enviados por e-mail |mail:]. +- Às vezes, o armazenamento [em cache |caching:] pode ser útil, por exemplo, ao baixar e exibir feeds. + +Na era atual, em que velocidade e eficiência são fundamentais, é importante ter ferramentas que permitam obter resultados sem atrasos desnecessários. A estrutura Nette oferece exatamente isso: desenvolvimento rápido, segurança e uma ampla variedade de ferramentas, como Tracy e Latte, que simplificam o processo. Basta instalar alguns pacotes Nette e criar um microsite como esse se torna muito fácil. E você sabe que não há falhas de segurança ocultas. diff --git a/best-practices/pt/pagination.texy b/best-practices/pt/pagination.texy index dfce31dfc6..3d7ae8951f 100644 --- a/best-practices/pt/pagination.texy +++ b/best-practices/pt/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository No Apresentador injetamos então a classe do modelo e no método de renderização pediremos os artigos publicados que passamos para o modelo: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -No modelo, nós nos encarregaremos de elaborar uma lista de artigos: +O modelo `default.latte` se encarregará de listar os artigos: ```latte {block content} @@ -114,7 +113,7 @@ O próximo passo é editar o apresentador. Nós encaminharemos o número da pág Também expandimos o método de renderização para obter a instância Paginator, configurando-a e selecionando os artigos corretos a serem exibidos no modelo. Home PagePresenter terá este aspecto: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ O modelo já itera sobre artigos em uma página, basta adicionar links de pagina ``` -Foi assim que adicionamos a paginação usando o Paginador. Se for usado o [Nette Database Explorer |database:explorer] em vez do [Nette Database Core |database:core] como camada de banco de dados, podemos implementar paginação mesmo sem o Paginator. A classe `Nette\Database\Table\Selection` contém o método de [paginação |api:Nette\Database\Table\Selection::_ page] com lógica de paginação extraída do Paginator. +Foi assim que adicionamos a paginação usando o Paginador. Se for usado o [Nette Database Explorer |database:explorer] em vez do [Nette Database Core |database:sql-way] como camada de banco de dados, podemos implementar paginação mesmo sem o Paginator. A classe `Nette\Database\Table\Selection` contém o método de [paginação |api:Nette\Database\Table\Selection::_ page] com lógica de paginação extraída do Paginator. O repositório terá este aspecto: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository Não temos que criar o Paginador no Apresentador, em vez disso usaremos o método do objeto `Selection` devolvido pelo repositório: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/pt/post-links.texy b/best-practices/pt/post-links.texy new file mode 100644 index 0000000000..453fc217e1 --- /dev/null +++ b/best-practices/pt/post-links.texy @@ -0,0 +1,59 @@ +Como usar corretamente os links POST +************************************ + +Em aplicativos da Web, especialmente em interfaces administrativas, deve ser uma regra básica que as ações que alteram o estado do servidor não sejam executadas por meio do método HTTP GET. Como o nome do método sugere, o GET deve ser usado somente para recuperar dados, não para alterá-los. +Para ações como a exclusão de registros, é mais apropriado usar o método POST. Embora o ideal fosse usar o método DELETE, ele não pode ser invocado sem JavaScript, por isso o POST é historicamente usado. + +Como fazer isso na prática? Use este truque simples. No início do seu modelo, crie um formulário auxiliar com o identificador `postForm`, que você usará para os botões de exclusão: + +```latte .{file:@layout.latte} +
    +``` + +Com esse formulário, você pode usar um `
    @@ -36,10 +38,12 @@ Formulare Comună ------ - [Cum se încarcă fișierul de configurare |bootstrap:] +- [Cum să scrieți microsite-uri |microsites] - [De ce Nette folosește notația constantă PascalCase? |https://blog.nette.org/ro/pentru-mai-putine-tipete-in-cod] - [De ce Nette nu folosește sufixul Interface? |https://blog.nette.org/ro/prefixele-si-sufixele-nu-se-regasesc-in-numele-interfetelor] - [Sfaturi pentru utilizarea Composer |composer] - [Sfaturi privind editorii și instrumentele |editors-and-tools] +- [Introducere în programarea orientată pe obiecte |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/ro/attribute-requires.texy b/best-practices/ro/attribute-requires.texy new file mode 100644 index 0000000000..7d541aaa50 --- /dev/null +++ b/best-practices/ro/attribute-requires.texy @@ -0,0 +1,179 @@ +Cum se utilizează `#[Requires]` Atributul +***************************************** + +.[perex] +Atunci când scrieți o aplicație web, vă confruntați adesea cu necesitatea de a restricționa accesul la anumite părți ale aplicației. Poate doriți ca unele cereri să poată trimite date doar prin intermediul unui formular (utilizând astfel metoda POST) sau să fie accesibile doar apelurilor AJAX. În Nette Framework 3.2, a fost introdus un nou instrument care vă permite să stabiliți astfel de restricții în mod elegant și clar: instrumentul `#[Requires]` atribut. + +Atributul este un marker special în PHP, pe care îl adăugați înainte de definiția unei clase sau metode. Deoarece este în esență o clasă, trebuie să includeți clauza use pentru ca următoarele exemple să funcționeze: + +```php +use Nette\Application\Attributes\Requires; +``` + +Puteți utiliza funcția `#[Requires]` cu clasa presenter în sine și cu aceste metode: + +- `action()` +- `render()` +- `handle()` +- `createComponent()` + +Ultimele două metode se referă, de asemenea, la componente, astfel încât puteți utiliza atributul și cu acestea. + +În cazul în care condițiile specificate de atribut nu sunt îndeplinite, se declanșează o eroare HTTP 4xx. + + +Metode HTTP .[#toc-http-methods] +-------------------------------- + +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 numai 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ă folosiți POST în loc de GET pentru acțiunile de schimbare a stării și cum să faceți acest lucru? [Citiți ghidul |post-links]. + +Puteți specifica o metodă sau o serie de metode. Un caz special este valoarea `'*'` pentru a activa toate metodele, pe care prezentatorii nu o permit în mod implicit din [motive de securitate |application:presenters#http-method-check]. + + +Apeluri AJAX .[#toc-ajax-calls] +------------------------------- + +Dacă doriți ca un prezentator sau o metodă să fie accesibilă numai pentru cererile AJAX, utilizați: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +Aceeași origine .[#toc-same-origin] +----------------------------------- + +Pentru a spori securitatea, puteți solicita ca solicitarea să fie făcută din același domeniu. Acest lucru previne [vulnerabilitatea la CSRF |nette:vulnerability-protection#cross-site-request-forgery-csrf]: + +```php +#[Requires(sameOrigin: true)] +class SecurePresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Pentru `handle()` este necesar în mod automat accesul din același domeniu. Prin urmare, dacă doriți să permiteți accesul din orice domeniu, specificați: + +```php +#[Requires(sameOrigin: false)] +public function handleList(): void +{ +} +``` + + +Acces prin Forward .[#toc-access-via-forward] +--------------------------------------------- + +Uneori este util să se restricționeze accesul la un prezentator astfel încât acesta să fie disponibil doar indirect, de exemplu, prin utilizarea metodelor `forward()` sau `switch()` de la un alt prezentator. Acesta este modul în care sunt protejați prezentatorii de erori pentru a împiedica declanșarea lor de la un URL: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +În practică, este adesea necesar să se marcheze anumite vizualizări care pot fi accesate numai pe baza logicii din prezentator. Din nou, pentru ca acestea să nu poată 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 .[#toc-specific-actions] +------------------------------------------ + +Puteți, de asemenea, să restricționați accesul la anumite coduri, cum ar fi crearea unei componente, numai pentru anumite acțiuni din prezentator: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +Pentru o singură acțiune, nu este nevoie să scrieți o matrice: `#[Requires(actions: 'default')]` + + +Atribute personalizate .[#toc-custom-attributes] +------------------------------------------------ + +Dacă doriți să utilizați `#[Requires]` în mod repetat cu aceleași setări, puteți crea propriul atribut care va moșteni atributul `#[Requires]` și să îl setați în funcție de nevoile dumneavoastră. + +De exemplu, `#[SingleAction]` permite accesul numai prin intermediul acțiunii `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 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 .[#toc-conclusion] +---------------------------- + +The `#[Requires]` vă oferă o mare flexibilitate și control asupra modului în care sunt accesate paginile dvs. web. Utilizând reguli simple, dar puternice, puteți spori securitatea și buna funcționare a aplicației dumneavoastră. După cum puteți vedea, utilizarea atributelor în Nette nu numai că vă poate simplifica munca, dar o poate și securiza. + +{{sitename: Best Practices}} diff --git a/best-practices/ro/composer.texy b/best-practices/ro/composer.texy index ef98ebdb69..0905a7c202 100644 --- a/best-practices/ro/composer.texy +++ b/best-practices/ro/composer.texy @@ -142,6 +142,12 @@ Sau direct în fișierul `composer.json`: ``` +Ignorarea versiunii PHP .[#toc-ignoring-php-version] +==================================================== + +Pachetele specifică de obicei atât cea mai mică versiune de PHP cu care sunt compatibile, cât și cea mai mare versiune cu care au fost testate. Dacă intenționați să utilizați o versiune chiar mai nouă de PHP, poate în scopuri de testare, Composer va refuza să instaleze un astfel de pachet. Soluția este de a utiliza opțiunea `--ignore-platform-req=php+`, care face ca Composer să ignore limitele superioare ale versiunii PHP solicitate. + + Rapoarte false .[#toc-false-reports] ==================================== @@ -183,7 +189,7 @@ Cu toate acestea, este de asemenea posibil să utilizați Composer pentru a înc Ulterior, este necesar să executați comanda `composer dumpautoload` cu fiecare modificare și să lăsați tabelele de autoloading să se regenereze. Acest lucru este extrem de incomod și este mult mai bine să încredințați această sarcină lui [RobotLoader |robot-loader:], care efectuează aceeași activitate în mod automat în fundal și mult mai rapid. -A doua opțiune este să urmați [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Spunând simplu, este un sistem în care spațiile de nume și numele claselor corespund structurii directoarelor și numelor de fișiere, adică `App\Router\RouterFactory` se află în fișierul `/path/to/App/Router/RouterFactory.php`. Exemplu de configurare: +A doua opțiune este să urmați [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Spunând simplu, este un sistem în care spațiile de nume și numele claselor corespund structurii directoarelor și numelor de fișiere, adică `App\Core\RouterFactory` se află în fișierul `/path/to/App/Core/RouterFactory.php`. Exemplu de configurare: ```js { diff --git a/best-practices/ro/dynamic-snippets.texy b/best-practices/ro/dynamic-snippets.texy index c5201295d5..3635169fd9 100644 --- a/best-practices/ro/dynamic-snippets.texy +++ b/best-practices/ro/dynamic-snippets.texy @@ -35,7 +35,7 @@ public function handleUnlike(int $articleId): void Ajaxizare .[#toc-ajaxization] ============================= -Să aducem acum AJAX în această aplicație simplă. Modificarea ratingului unui articol nu este suficient de importantă pentru a necesita o cerere HTTP cu redirecționare, așa că, în mod ideal, ar trebui să se facă cu AJAX în fundal. Vom folosi [scriptul handler din add-ons |https://componette.org/vojtech-dobes/nette.ajax.js/] cu convenția obișnuită ca legăturile AJAX să aibă clasa CSS `ajax`. +Să aducem acum AJAX în această aplicație simplă. Modificarea ratingului unui articol nu este suficient de importantă pentru a necesita o cerere HTTP cu redirecționare, așa că, în mod ideal, ar trebui să se facă cu AJAX în fundal. Vom folosi [scriptul handler din add-ons |application:ajax#toc-naja] cu convenția obișnuită ca legăturile AJAX să aibă clasa CSS `ajax`. Totuși, cum să o facem în mod specific? Nette oferă 2 modalități: modalitatea cu fragmente dinamice și modalitatea cu componente. Ambele au avantajele și dezavantajele lor, așa că le vom prezenta pe rând. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ Bineînțeles că vom schimba șablonul de vizualizare și va trebui să adăug ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/ro/form-reuse.texy b/best-practices/ro/form-reuse.texy index 0f1e60600b..4c9d7dede0 100644 --- a/best-practices/ro/form-reuse.texy +++ b/best-practices/ro/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -Este foarte important ca legătura dintre clasele `FormFactory` și `EditFormFactory` să fie implementată prin compoziție, nu prin moștenirea obiectelor: +Este foarte important ca legătura dintre clasele `FormFactory` și `EditFormFactory` să fie implementată [prin compoziție |nette:introduction-to-object-oriented-programming#composition], nu prin [moștenirea obiectelor |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance]: ```php // ⛔ NU! MOȘTENIREA NU ARE CE CĂUTA AICI @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory Utilizarea moștenirii în acest caz ar fi complet contraproductivă. Ați întâmpina foarte repede probleme. De exemplu, dacă ați dori să adăugați parametri la metoda `create()`, PHP ar raporta o eroare deoarece semnătura acesteia este diferită de cea a metodei părinte. Sau atunci când treceți o dependență clasei `EditFormFactory` prin intermediul constructorului. Acest lucru ar cauza ceea ce numim " [iadul constructorilor |dependency-injection:passing-dependencies#Constructor hell]". -În general, este mai bine să preferați compoziția decât moștenirea. +În general, este mai bine să se prefere [compoziția decât moștenirea |dependency-injection:faq#Why composition is preferred over inheritance]. Gestionarea formularelor .[#toc-form-handling] @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/ro/lets-create-contact-form.texy b/best-practices/ro/lets-create-contact-form.texy index eecffd9791..42435507e4 100644 --- a/best-practices/ro/lets-create-contact-form.texy +++ b/best-practices/ro/lets-create-contact-form.texy @@ -39,7 +39,7 @@ După cum puteți vedea, am creat două metode. Prima metodă `createComponentCo Dar ce se întâmplă dacă utilizatorul nu completează unele câmpuri? În acest caz, ar trebui să-l anunțăm că este un câmp obligatoriu. Am făcut acest lucru cu metoda `setRequired()`. În cele din urmă, am adăugat și un [eveniment |nette:glossary#events] `onSuccess`, care este declanșat dacă formularul este trimis cu succes. În cazul nostru, acesta apelează metoda `contactFormSucceeded`, care se ocupă de procesarea formularului trimis. Vom adăuga acest lucru în cod imediat. -Lăsați componenta `contantForm` să fie redată în șablonul `templates/Home/default.latte`: +Lăsați componenta `contantForm` să fie redată în șablonul `Home/default.latte`: ```latte {block content} diff --git a/best-practices/ro/microsites.texy b/best-practices/ro/microsites.texy new file mode 100644 index 0000000000..bdeb94a83c --- /dev/null +++ b/best-practices/ro/microsites.texy @@ -0,0 +1,63 @@ +Cum să scrieți microsite-uri +**************************** + +Imaginați-vă că trebuie să creați rapid un mic site web pentru un eveniment viitor al companiei dumneavoastră. Acesta trebuie să fie simplu, rapid și fără complicații inutile. S-ar putea să vă gândiți că un proiect atât de mic nu necesită un framework robust. Dar dacă utilizarea cadrului Nette ar putea simplifica și accelera semnificativ acest proces? + +Chiar și atunci când creați site-uri web simple, nu doriți să renunțați la confort. Nu doriți să reinventați roata. Simțiți-vă liber să fiți leneș și să vă răsfățați. Cadrul Nette Framework poate fi folosit excelent și ca microcadru. + +Cum ar putea arăta un astfel de microsite? De exemplu, întregul cod al site-ului poate fi plasat într-un singur fișier `index.php` în folderul public: + +```php +<?php + +require __DIR__ . '/../vendor/autoload.php'; + +$configurator = new Nette\Bootstrap\Configurator; +$configurator->enableTracy(__DIR__ . '/../log'); +$configurator->setTempDirectory(__DIR__ . '/../temp'); + +// creați un container DI pe baza configurației din config.neon +$configurator->addConfig(__DIR__ . '/../app/config.neon'); +$container = $configurator->createContainer(); + +// configurați rutarea +$router = new Nette\Application\Routers\RouteList; +$container->addService('router', $router); + +// ruta pentru URL https://example.com/ +$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { + // detectează limba browserului și redirecționează la URL /en sau /de etc. + $supportedLangs = ['en', 'de', 'cs']; + $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); + $presenter->redirectUrl("/$lang"); +}); + +// ruta pentru URL https://example.com/cs nebo https://example.com/en +$router->addRoute('', function ($presenter, string $lang) { + // afișează șablonul corespunzător, de exemplu ../template/ro.latte + $template = $presenter->createTemplate() + ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); + return $template; +}); + +// rulați aplicația! +$container->getByType(Nette\Application\Application::class)->run(); +``` + +Restul vor fi șabloane stocate în dosarul părinte `/templates`. + +Codul PHP din `index.php` [stabilește |bootstrap:] mai întâi [mediul |bootstrap:], apoi definește [rutele |application:routing#dynamic-routing-with-callbacks] și, în final, rulează aplicația. Avantajul este că al doilea parametru al funcției `addRoute()` poate fi un callable care este executat atunci când se deschide pagina corespunzătoare. + + +De ce să folosiți Nette pentru microsite-uri? .[#toc-why-use-nette-for-microsites] +---------------------------------------------------------------------------------- + +- Dezvoltatorii care au încercat vreodată [Tracy |tracy:] nu-și pot imagina astăzi codarea fără el. +- Dar, mai presus de toate, veți utiliza sistemul de template-uri [Latte |latte:], deoarece de la doar 2 pagini, veți dori să separați [aspectul și conținutul |latte:template-inheritance]. +- Și, cu siguranță, veți dori să vă bazați pe [escape-ul automat |latte:safety-first] pentru a preveni vulnerabilitățile XSS. +- De asemenea, Latte se asigură că, în caz de eroare, nu vor fi afișate niciodată mesaje de eroare PHP, ci în schimb, o pagină ușor de utilizat. +- Dacă doriți să obțineți feedback de la utilizatori, de exemplu sub forma unui formular de contact, puteți adăuga, de asemenea, [formulare |forms:] și [bază de date |database:]. +- De asemenea, puteți face cu ușurință ca formularele completate să fie [trimise prin e-mail |mail:]. +- Uneori s-ar putea să vă fie utilă [memoria cache |caching:], de exemplu, atunci când descărcați și afișați fluxuri. + +În epoca actuală, în care viteza și eficiența sunt esențiale, este important să dispuneți de instrumente care vă permit să obțineți rezultate fără întârzieri inutile. Cadrul Nette oferă exact acest lucru - dezvoltare rapidă, securitate și o gamă largă de instrumente precum Tracy și Latte, care simplifică procesul. Este suficient să instalați câteva pachete Nette, iar construirea unui astfel de microsite devine o joacă de copii. Și știți că nu există defecte de securitate ascunse. diff --git a/best-practices/ro/pagination.texy b/best-practices/ro/pagination.texy index 4175e89bd9..f07b2c4eb4 100644 --- a/best-practices/ro/pagination.texy +++ b/best-practices/ro/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository În Presenter vom injecta apoi clasa model, iar în metoda render vom cere articolele publicate pe care le vom trece în șablon: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -În șablon, ne vom ocupa de redarea unei liste de articole: +Șablonul `default.latte` se va ocupa apoi de listarea articolelor: ```latte {block content} @@ -114,7 +113,7 @@ Următorul pas este să modificăm prezentatorul. Vom transmite numărul paginii De asemenea, extindem metoda de randare pentru a obține instanța Paginator, configurând-o și selectând articolele corecte pentru a fi afișate în șablon. HomePresenter va arăta astfel: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ class HomePresenter extends Nette\Application\UI\Presenter ``` -Acesta este modul în care am adăugat paginarea folosind Paginator. Dacă [Nette |database:core] [Database Explorer |database:explorer] este utilizat în locul [Nette Database Core |database:core] ca strat de bază de date, putem implementa paginarea chiar și fără Paginator. Clasa `Nette\Database\Table\Selection` conține metoda [page |api:Nette\Database\Table\Selection::_ page] cu logica de paginare preluată din Paginator. +Acesta este modul în care am adăugat paginarea folosind Paginator. Dacă [Nette |database:sql-way] [Database Explorer |database:explorer] este utilizat în locul [Nette Database Core |database:sql-way] ca strat de bază de date, putem implementa paginarea chiar și fără Paginator. Clasa `Nette\Database\Table\Selection` conține metoda [page |api:Nette\Database\Table\Selection::_ page] cu logica de paginare preluată din Paginator. Depozitul va arăta astfel: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository Nu trebuie să creăm Paginator în Presenter, în schimb vom folosi metoda obiectului `Selection` returnat de depozit: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/ro/post-links.texy b/best-practices/ro/post-links.texy new file mode 100644 index 0000000000..d81e2aa8bb --- /dev/null +++ b/best-practices/ro/post-links.texy @@ -0,0 +1,59 @@ +Cum să folosiți corect legăturile POST +************************************** + +Î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 să fie utilizată numai pentru a prelua date, nu pentru a le modifica. +Pentru acțiuni precum ștergerea înregistrărilor, este mai indicat să se utilizeze metoda POST. Deși ideal ar fi să se folosească metoda DELETE, aceasta nu poate fi invocată fără JavaScript, de aceea se folosește în mod obișnuit metoda POST. + +Cum se procedează în practică? Folosiți acest truc simplu. La începutul șablonului dumneavoastră, creați un formular ajutător cu identificatorul `postForm`, pe care îl veți folosi apoi pentru butoanele de ștergere: + +```latte .{file:@layout.latte} +
    +``` + +Cu acest formular, puteți utiliza un `
    @@ -36,10 +38,12 @@ Общие ----- - [Как загрузить файл конфигурации |bootstrap:] +- [Как писать микросайты |microsites] - [Почему Nette использует константную нотацию PascalCase? |https://blog.nette.org/ru/ctoby-men-se-kricat-v-kode] - [Почему Nette не использует суффикс Interface? |https://blog.nette.org/ru/prefiksy-i-suffiksy-ne-dolzny-prisutstvovat-v-imenah-interfejsov] - [Советы по использованию Composer |composer] - [Советы по редакторам и инструментам |editors-and-tools] +- [Введение в объектно-ориентированное программирование |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/ru/attribute-requires.texy b/best-practices/ru/attribute-requires.texy new file mode 100644 index 0000000000..f91273fa06 --- /dev/null +++ b/best-practices/ru/attribute-requires.texy @@ -0,0 +1,179 @@ +Как использовать `#[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 .[#toc-http-methods] +-------------------------------- + +Вы можете указать, какие методы 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-method-check]. + + +Вызовы AJAX .[#toc-ajax-calls] +------------------------------ + +Если вы хотите, чтобы ведущий или метод был доступен только для AJAX-запросов, используйте: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +То же происхождение .[#toc-same-origin] +--------------------------------------- + +Для повышения безопасности можно потребовать, чтобы запрос выполнялся из одного и того же домена. Это предотвратит [уязвимость к 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 .[#toc-access-via-forward] +----------------------------------------------- + +Иногда полезно ограничить доступ к презентатору так, чтобы он был доступен только косвенно, например, с помощью методов `forward()` или `switch()` из другого презентатора. Так защищаются презентаторы ошибок, чтобы их нельзя было вызвать с URL: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\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 + { + } +} +``` + + +Конкретные действия .[#toc-specific-actions] +-------------------------------------------- + +Вы также можете ограничить доступ к определенному коду, например к созданию компонента, только для определенных действий в презентере: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +Для одного действия нет необходимости писать массив: `#[Requires(actions: 'default')]` + + +Пользовательские атрибуты .[#toc-custom-attributes] +--------------------------------------------------- + +Если вы хотите использовать атрибут `#[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 +{ +} +``` + + +Заключение .[#toc-conclusion] +----------------------------- + +Атрибут `#[Requires]` предоставляет вам большую гибкость и контроль над тем, как осуществляется доступ к вашим веб-страницам. Используя простые, но мощные правила, вы можете повысить безопасность и правильное функционирование вашего приложения. Как видите, использование атрибутов в Nette может не только упростить вашу работу, но и обезопасить ее. + +{{sitename: Best Practices}} diff --git a/best-practices/ru/composer.texy b/best-practices/ru/composer.texy index 8b6c3fca4a..6e77697023 100644 --- a/best-practices/ru/composer.texy +++ b/best-practices/ru/composer.texy @@ -142,6 +142,12 @@ composer require php 8.2.3 --no-update ``` +Игнорирование версии PHP .[#toc-ignoring-php-version] +===================================================== + +В пакетах обычно указывается как самая низкая версия PHP, с которой они совместимы, так и самая высокая версия, с которой они были протестированы. Если вы планируете использовать еще более новую версию PHP, возможно, в целях тестирования, Composer откажется устанавливать такой пакет. Решением является использование опции `--ignore-platform-req=php+`, которая заставляет Composer игнорировать верхние границы требуемой версии PHP. + + Ложные отчеты .[#toc-false-reports] =================================== @@ -183,7 +189,7 @@ Packagist.org — глобальный репозиторий .[#toc-packagist-o Впоследствии необходимо выполнять команду `composer dumpautoload` при каждом изменении и позволять таблицам автозагрузки регенерироваться. Это крайне неудобно, и гораздо лучше доверить эту задачу [RobotLoader|robot-loader:], который выполняет ту же самую работу автоматически в фоновом режиме и гораздо быстрее. -Второй вариант — следовать [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Проще говоря, это система, в которой пространства имен и имена классов соответствуют структуре каталогов и именам файлов, т. е. `App\Router\RouterFactory` находится в файле `/path/to/App/Router/RouterFactory.php`. Пример конфигурации: +Второй вариант — следовать [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Проще говоря, это система, в которой пространства имен и имена классов соответствуют структуре каталогов и именам файлов, т. е. `App\Core\RouterFactory` находится в файле `/path/to/App/Core/RouterFactory.php`. Пример конфигурации: ```js { diff --git a/best-practices/ru/dynamic-snippets.texy b/best-practices/ru/dynamic-snippets.texy index 744b905458..2d90f071b4 100644 --- a/best-practices/ru/dynamic-snippets.texy +++ b/best-practices/ru/dynamic-snippets.texy @@ -35,7 +35,7 @@ Template: Аяксизация .[#toc-ajaxization] ============================== -Теперь давайте привнесем AJAX в это простое приложение. Изменение рейтинга статьи не настолько важно, чтобы требовать HTTP-запрос с перенаправлением, поэтому в идеале это должно быть сделано с помощью AJAX в фоновом режиме. Мы будем использовать [скрипт обработчика из дополнений |https://componette.org/vojtech-dobes/nette.ajax.js/] с обычным соглашением, что AJAX ссылки имеют CSS класс `ajax`. +Теперь давайте привнесем AJAX в это простое приложение. Изменение рейтинга статьи не настолько важно, чтобы требовать HTTP-запрос с перенаправлением, поэтому в идеале это должно быть сделано с помощью AJAX в фоновом режиме. Мы будем использовать [скрипт обработчика из дополнений |application:ajax#toc-naja] с обычным соглашением, что AJAX ссылки имеют CSS класс `ajax`. Однако как это сделать конкретно? Nette предлагает 2 способа: способ динамических фрагментов и способ компонентов. У обоих есть свои плюсы и минусы, поэтому мы покажем их по очереди. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ class LikeControl extends Nette\Application\UI\Control ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/ru/form-reuse.texy b/best-practices/ru/form-reuse.texy index 534ce944d8..f79fc541af 100644 --- a/best-practices/ru/form-reuse.texy +++ b/best-practices/ru/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -Очень важно, чтобы связь между классами `FormFactory` и `EditFormFactory` была реализована композицией, а не наследованием объектов: +Очень важно, что привязка между классами `FormFactory` и `EditFormFactory` осуществляется [путем композиции |nette:introduction-to-object-oriented-programming#composition], а не [наследования объектов |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance]: ```php // ⛔ НЕТ! НАСЛЕДСТВУ ЗДЕСЬ НЕ МЕСТО @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory Использование наследования в этом случае было бы совершенно непродуктивным. Вы бы очень быстро столкнулись с проблемами. Например, если бы вы захотели добавить параметры в метод `create()`, PHP выдал бы ошибку, что его сигнатура отличается от родительской. Или при передаче зависимости классу `EditFormFactory` через конструктор. Это привело бы к тому, что мы называем " [ад конструктора |dependency-injection:passing-dependencies#Constructor hell]". -В целом, лучше предпочесть композицию наследованию. +В целом лучше отдать предпочтение [композиции, а не наследованию |dependency-injection:faq#Why composition is preferred over inheritance]. Работа с формами .[#toc-form-handling] @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/ru/lets-create-contact-form.texy b/best-practices/ru/lets-create-contact-form.texy index f81335f4ef..bed8cf51df 100644 --- a/best-practices/ru/lets-create-contact-form.texy +++ b/best-practices/ru/lets-create-contact-form.texy @@ -39,7 +39,7 @@ class HomePresenter extends Presenter Но что, если пользователь не заполнит некоторые поля? В этом случае мы должны сообщить ему, что это обязательное поле. Мы сделали это с помощью метода `setRequired()`. Наконец, мы также добавили [событие |nette:glossary#events] `onSuccess`, которое срабатывает в случае успешной отправки формы. В нашем случае оно вызывает метод `contactFormSucceeded`, который обрабатывает отправленную форму. Мы добавим это в код через некоторое время. -Пусть компонент `contantForm` будет отображен в шаблоне `templates/Home/default.latte`: +Пусть компонент `contantForm` будет отображен в шаблоне `Home/default.latte`: ```latte {block content} diff --git a/best-practices/ru/microsites.texy b/best-practices/ru/microsites.texy new file mode 100644 index 0000000000..4db9a76100 --- /dev/null +++ b/best-practices/ru/microsites.texy @@ -0,0 +1,63 @@ +Как писать микросайты +********************* + +Представьте себе, что вам нужно быстро создать небольшой сайт для предстоящего мероприятия вашей компании. Он должен быть простым, быстрым и без лишних сложностей. Вы можете подумать, что для такого небольшого проекта не нужен надежный фреймворк. Но что если использование фреймворка 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 nebo https://example.com/en +$router->addRoute('', 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#dynamic-routing-with-callbacks] и, наконец, запускает приложение. Преимуществом является то, что вторым параметром функции `addRoute()` может быть вызываемая переменная, которая выполняется при открытии соответствующей страницы. + + +Почему стоит использовать Nette для микросайтов? .[#toc-why-use-nette-for-microsites] +------------------------------------------------------------------------------------- + +- Разработчики, которые когда-либо пробовали [Tracy |tracy:], сегодня не могут представить себе кодирование без нее. +- Но прежде всего вам пригодится система шаблонов [Latte |latte:], поскольку всего с 2 страниц вы захотите разделить [верстку и контент |latte:template-inheritance]. +- А для предотвращения XSS-уязвимостей вам обязательно понадобится [автоматическое экранирование |latte:safety-first]. +- Nette также гарантирует, что в случае ошибки никогда не будут выводиться сообщения об ошибках PHP, а вместо них будет отображаться удобная для пользователя страница. +- Если вы хотите получать обратную связь от пользователей, например, в виде контактной формы, вы также можете добавить [формы |forms:] и [базу данных |database:]. +- Кроме того, заполненные формы можно легко отправить по [электронной почте |mail:]. +- Иногда может пригодиться [кэширование |caching:], например, при загрузке и отображении лент. + +В наше время, когда скорость и эффективность являются ключевыми факторами, важно иметь инструменты, позволяющие достигать результатов без лишних задержек. Фреймворк Nette предлагает именно такие инструменты - быструю разработку, безопасность и широкий набор средств, таких как Tracy и Latte, которые упрощают процесс. Достаточно установить несколько пакетов Nette, и создание такого микросайта превращается в легкое дело. При этом вы будете уверены, что в системе безопасности нет никаких скрытых недостатков. diff --git a/best-practices/ru/pagination.texy b/best-practices/ru/pagination.texy index 11f52f40f2..913f2f3c53 100644 --- a/best-practices/ru/pagination.texy +++ b/best-practices/ru/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository Затем в презентере мы вводим класс модели и в методе `render` запрашиваем опубликованные статьи, которые передаем в шаблон: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -В шаблоне мы позаботимся о выводе списка статей: +Шаблон `default.latte` позаботится о перечислении статей: ```latte {block content} @@ -114,7 +113,7 @@ class ArticleRepository Мы также расширяем метод `render` для получения экземпляра Paginator, его настройки и выбора нужных статей для отображения в шаблоне. HomePresenter будет выглядеть следующим образом: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ class HomePresenter extends Nette\Application\UI\Presenter ``` -Вот как мы добавили пагинацию с помощью Paginator. Если вместо [Nette Database Explorer |database:explorer] в качестве слоя базы данных используется [Nette Database Core |database:core], мы можем реализовать подкачку даже без Paginator. Класс `Nette\Database\Table\Selection` содержит метод [page |api:Nette\Database\Table\Selection::_ page] с логикой пагинации, взятой из Paginator. +Вот как мы добавили пагинацию с помощью Paginator. Если вместо [Nette Database Explorer |database:explorer] в качестве слоя базы данных используется [Nette Database Core |database:sql-way], мы можем реализовать подкачку даже без Paginator. Класс `Nette\Database\Table\Selection` содержит метод [page |api:Nette\Database\Table\Selection::_ page] с логикой пагинации, взятой из Paginator. Репозиторий будет выглядеть следующим образом: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository Нам не нужно создавать Paginator в презентере, вместо этого мы будем использовать метод объекта `Selection`, возвращаемый репозиторием: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/ru/post-links.texy b/best-practices/ru/post-links.texy new file mode 100644 index 0000000000..ac941e667a --- /dev/null +++ b/best-practices/ru/post-links.texy @@ -0,0 +1,59 @@ +Как правильно использовать POST-ссылки +************************************** + +В веб-приложениях, особенно в административных интерфейсах, должно быть основным правилом, что действия, изменяющие состояние сервера, не должны выполняться с помощью метода HTTP GET. Как следует из названия метода, GET должен использоваться только для получения данных, а не для их изменения. +Для таких действий, как удаление записей, целесообразнее использовать метод POST. Хотя идеальным вариантом было бы использование метода DELETE, его невозможно вызвать без JavaScript, поэтому исторически используется POST. + +Как сделать это на практике? Используйте этот простой прием. В начале вашего шаблона создайте вспомогательную форму с идентификатором `postForm`, которую вы затем будете использовать для кнопок удаления: + +```latte .{file:@layout.latte} +
    +``` + +В этой форме вы можете использовать `
    @@ -36,10 +38,12 @@ Obrazci Skupna spletna stran -------------------- - [Kako naložiti konfiguracijsko datoteko |bootstrap:] +- [Kako napisati mikrostrani |microsites] - [Zakaj Nette uporablja konstantni zapis PascalCase |https://blog.nette.org/sl/za-manj-kricanja-v-kodi]? - [Zakaj Nette ne uporablja končnice Interface |https://blog.nette.org/sl/predpone-in-pripone-ne-sodijo-v-imena-vmesnikov]? - [Nasveti za uporabo programa Composer |composer] - [Nasveti o urejevalnikih in orodjih |editors-and-tools] +- [Uvod v objektno usmerjeno programiranje |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/sl/attribute-requires.texy b/best-practices/sl/attribute-requires.texy new file mode 100644 index 0000000000..9c1494eb09 --- /dev/null +++ b/best-practices/sl/attribute-requires.texy @@ -0,0 +1,179 @@ +Kako uporabljati `#[Requires]` Atribut +************************************** + +.[perex] +Pri pisanju spletne aplikacije se pogosto srečate s potrebo po omejitvi dostopa do določenih delov aplikacije. Morda želite, da lahko nekatere zahteve pošljejo podatke samo prek obrazca (torej z uporabo metode POST) ali da so dostopne samo za klice AJAX. V okolju Nette Framework 3.2 je bilo uvedeno novo orodje, ki vam omogoča elegantno in jasno določanje takšnih omejitev: orodje `#[Requires]` atribut. + +Atribut je posebna oznaka v jeziku PHP, ki jo dodate pred definicijo razreda ali metode. Ker gre v bistvu za razred, morate za delovanje naslednjih primerov vključiti klavzulo use: + +```php +use Nette\Application\Attributes\Requires; +``` + +Uporabite lahko `#[Requires]` atribut v samem razredu presenter in v teh metodah: + +- `action()` +- `render()` +- `handle()` +- `createComponent()` + +Tudi zadnji dve metodi se nanašata na komponente, zato lahko atribut uporabite tudi z njima. + +Če pogoji, določeni z atributom, niso izpolnjeni, se sproži napaka HTTP 4xx. + + +Metode HTTP .[#toc-http-methods] +-------------------------------- + +Določite lahko, katere metode HTTP (kot so GET, POST itd.) so dovoljene za dostop. Če želite na primer dovoliti dostop samo z oddajo obrazca, nastavite: + +```php +class AdminPresenter extends Nette\Application\UI\Presenter +{ + #[Requires(methods: 'POST')] + public function actionDelete(int $id): void + { + } +} +``` + +Zakaj za spreminjanje stanja uporabiti POST namesto GET in kako to storiti? [Preberite vodnik |post-links]. + +Določite lahko metodo ali niz metod. Poseben primer je vrednost `'*'` za omogočanje vseh metod, ki jih predstavniki zaradi [varnostnih razlogov |application:presenters#http-method-check] privzeto ne omogočajo. + + +Klici AJAX .[#toc-ajax-calls] +----------------------------- + +Če želite, da je predstavnik ali metoda dostopna samo za zahteve AJAX, uporabite: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +Enako poreklo .[#toc-same-origin] +--------------------------------- + +Če želite povečati varnost, lahko zahtevate, da je zahteva poslana 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 +{ +} +``` + +Za `handle()` je samodejno potreben dostop iz iste domene. Če torej želite dovoliti dostop iz katere koli domene, določite: + +```php +#[Requires(sameOrigin: false)] +public function handleList(): void +{ +} +``` + + +Dostop prek spletne strani Forward .[#toc-access-via-forward] +------------------------------------------------------------- + +Včasih je koristno omejiti dostop do predstavnika, tako da je na voljo le posredno, na primer z uporabo metod `forward()` ali `switch()` iz drugega predstavnika. Tako so predstavniki napak zaščiteni, da jih ni mogoče sprožiti z naslova URL: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +V praksi je pogosto treba označiti določene poglede, do katerih je mogoče dostopati le na podlagi logike v predstavitvenem programu. Spet tako, 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 + { + } +} +``` + + +Posebni ukrepi .[#toc-specific-actions] +--------------------------------------- + +Prav tako lahko omejite, da bo določena koda, na primer ustvarjanje komponente, dostopna samo za določena dejanja v predstavitvenem programu: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +Za posamezno dejanje ni treba pisati polja: `#[Requires(actions: 'default')]` + + +Atributi po meri .[#toc-custom-attributes] +------------------------------------------ + +Če želite uporabiti `#[Requires]` atribut večkrat uporabiti z enakimi nastavitvami, lahko ustvarite svoj atribut, ki bo podedoval `#[Requires]` in ga nastavite v skladu s svojimi potrebami. + +Na primer, `#[SingleAction]` omogoča dostop samo prek dejanja `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]` omogoči dostop prek vseh metod HTTP, ki se uporabljajo za API 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 +{ +} +``` + + +Zaključek .[#toc-conclusion] +---------------------------- + +Na spletni strani `#[Requires]` vam omogoča veliko prilagodljivost in nadzor nad načinom dostopa do spletnih strani. Z uporabo preprostih, a zmogljivih pravil lahko izboljšate varnost in pravilno delovanje svoje aplikacije. Kot lahko vidite, lahko z uporabo atributov v Nette ne le poenostavite svoje delo, temveč ga tudi zavarujete. + +{{sitename: Best Practices}} diff --git a/best-practices/sl/composer.texy b/best-practices/sl/composer.texy index 52eb9cc2da..6a3fa3ffdc 100644 --- a/best-practices/sl/composer.texy +++ b/best-practices/sl/composer.texy @@ -142,6 +142,12 @@ Ali neposredno v datoteki `composer.json`: ``` +Ignoriranje različice PHP .[#toc-ignoring-php-version] +====================================================== + +V paketih je običajno navedena najnižja različica PHP, s katero so združljivi, in najvišja različica, s katero so bili testirani. Če nameravate uporabiti še novejšo različico PHP, morda za namene testiranja, bo Composer zavrnil namestitev takega paketa. Rešitev je uporaba možnosti `--ignore-platform-req=php+`, ki povzroči, da Composer ne upošteva zgornje meje zahtevane različice PHP. + + Napačna poročila .[#toc-false-reports] ====================================== @@ -183,7 +189,7 @@ Vendar je mogoče Composer uporabiti tudi za nalaganje drugih razredov zunaj map Nato je treba ob vsaki spremembi zagnati ukaz `composer dumpautoload` in pustiti, da se tabele za samodejno nalaganje regenerirajo. To je izredno neprijetno in veliko bolje je to nalogo zaupati programu [RobotLoader |robot-loader:], ki isto dejavnost opravi samodejno v ozadju in veliko hitreje. -Druga možnost je, da sledite [priporočilu PSR-4 |https://www.php-fig.org/psr/psr-4/]. Preprosto povedano, gre za sistem, v katerem imenska območja in imena razredov ustrezajo imeniški strukturi in imenom datotek, tj. `App\Router\RouterFactory` se nahaja v datoteki `/path/to/App/Router/RouterFactory.php`. Primer konfiguracije: +Druga možnost je, da sledite [priporočilu PSR-4 |https://www.php-fig.org/psr/psr-4/]. Preprosto povedano, gre za sistem, v katerem imenska območja in imena razredov ustrezajo imeniški strukturi in imenom datotek, tj. `App\Core\RouterFactory` se nahaja v datoteki `/path/to/App/Core/RouterFactory.php`. Primer konfiguracije: ```js { diff --git a/best-practices/sl/dynamic-snippets.texy b/best-practices/sl/dynamic-snippets.texy index dcd25eb0c7..0554bab175 100644 --- a/best-practices/sl/dynamic-snippets.texy +++ b/best-practices/sl/dynamic-snippets.texy @@ -35,7 +35,7 @@ Predloga: Ajaksizacija .[#toc-ajaxization] ================================ -V to preprosto aplikacijo zdaj vnesimo AJAX. Spreminjanje ocene članka ni dovolj pomembno, da bi zahtevalo zahtevo HTTP s preusmeritvijo, zato bi bilo idealno, če bi to opravili z AJAXom v ozadju. Uporabili bomo [skript za obdelavo iz dodatkov |https://componette.org/vojtech-dobes/nette.ajax.js/] z običajno konvencijo, da imajo povezave AJAX razred CSS `ajax`. +V to preprosto aplikacijo zdaj vnesimo AJAX. Spreminjanje ocene članka ni dovolj pomembno, da bi zahtevalo zahtevo HTTP s preusmeritvijo, zato bi bilo idealno, če bi to opravili z AJAXom v ozadju. Uporabili bomo [skript za obdelavo iz dodatkov |application:ajax#toc-naja] z običajno konvencijo, da imajo povezave AJAX razred CSS `ajax`. Vendar pa, kako to storiti konkretno? Nette ponuja dva načina: način z dinamičnimi utrinki in način s komponentami. Oba načina imata svoje prednosti in slabosti, zato ju bomo prikazali enega za drugim. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ Seveda bomo spremenili predlogo pogleda in morali bomo dodati tovarno za predsta ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/sl/form-reuse.texy b/best-practices/sl/form-reuse.texy index 398e4490b0..7eaa8a578e 100644 --- a/best-practices/sl/form-reuse.texy +++ b/best-practices/sl/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -Zelo pomembno je, da se vezava med razredoma `FormFactory` in `EditFormFactory` izvaja s kompozicijo in ne z dedovanjem objektov: +Zelo pomembno je, da se povezava med razredoma `FormFactory` in `EditFormFactory` izvaja [s kompozicijo |nette:introduction-to-object-oriented-programming#composition] in ne z [dedovanjem objektov |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance]: ```php // ⛔ NE! DEDIŠČINA NE SPADA SEM @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory Uporaba dedovanja bi bila v tem primeru popolnoma neproduktivna. Zelo hitro bi naleteli na težave. Na primer, če bi želeli metodi `create()` dodati parametre; PHP bi sporočil napako, ker se njen podpis razlikuje od podpisa nadrejene metode. Ali pa pri posredovanju odvisnosti razredu `EditFormFactory` prek konstruktorja. To bi povzročilo tako imenovani [konstruktorski pekel |dependency-injection:passing-dependencies#Constructor hell]. -Na splošno je bolje dati prednost kompoziciji pred dedovanjem. +Na splošno je bolje dati prednost [sestavi pred dedovanjem |dependency-injection:faq#Why composition is preferred over inheritance]. Ravnanje z obrazci .[#toc-form-handling] @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/sl/lets-create-contact-form.texy b/best-practices/sl/lets-create-contact-form.texy index feeae86ee8..0fe2c371a9 100644 --- a/best-practices/sl/lets-create-contact-form.texy +++ b/best-practices/sl/lets-create-contact-form.texy @@ -39,7 +39,7 @@ Kot lahko vidite, smo ustvarili dve metodi. Prva metoda `createComponentContactF Kaj pa, če uporabnik ne izpolni nekaterih polj? V tem primeru mu moramo sporočiti, da gre za zahtevano polje. To smo storili z metodo `setRequired()`. Na koncu smo dodali še [dogodek |nette:glossary#events] `onSuccess`, ki se sproži, če je obrazec uspešno oddan. V našem primeru pokliče metodo `contactFormSucceeded`, ki poskrbi za obdelavo oddanega obrazca. To bomo v kodo dodali v naslednjem trenutku. -Naj bo komponenta `contantForm` prikazana v predlogi `templates/Home/default.latte`: +Naj bo komponenta `contantForm` prikazana v predlogi `Home/default.latte`: ```latte {block content} diff --git a/best-practices/sl/microsites.texy b/best-practices/sl/microsites.texy new file mode 100644 index 0000000000..ce6af7d0da --- /dev/null +++ b/best-practices/sl/microsites.texy @@ -0,0 +1,63 @@ +Kako napisati mikrostrani +************************* + +Predstavljajte si, da morate na hitro ustvariti majhno spletno stran za prihajajoči dogodek vašega podjetja. Biti mora preprosta, hitra in brez nepotrebnih zapletov. Morda mislite, da tako majhen projekt ne zahteva robustnega ogrodja. Kaj pa, če lahko z uporabo ogrodja Nette ta postopek bistveno poenostavite in pospešite? + +Tudi pri ustvarjanju preprostih spletnih mest se ne želite odpovedati udobju. Ne želite na novo izumljati kolesa. Privoščite si, da ste leni in se razvajate. Okvir Nette je mogoče odlično uporabiti tudi kot mikrookvir. + +Kako je lahko videti takšna mikrostrani? Celotno kodo spletnega mesta lahko na primer postavite 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'); + +// ustvarite vsebnik DI na podlagi konfiguracije v config.neon +$configurator->addConfig(__DIR__ . '/../app/config.neon'); +$container = $configurator->createContainer(); + +// nastavi usmerjanje +$router = new Nette\Application\Routers\RouteList; +$container->addService('router', $router); + +// pot za URL https://example.com/ +$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { + // zaznajo jezik brskalnika in preusmerijo 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('', function ($presenter, string $lang) { + // prikaže ustrezno predlogo, na primer ../templates/en.latte + $template = $presenter->createTemplate() + ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); + return $template; +}); + +// zaženite aplikacijo! +$container->getByType(Nette\Application\Application::class)->run(); +``` + +Vse drugo bodo predloge, shranjene v nadrejeni mapi `/templates`. + +Koda PHP v mapi `index.php` najprej vzpostavi [okolje |bootstrap:], nato definira [poti |application:routing#dynamic-routing-with-callbacks] in nazadnje zažene aplikacijo. Prednost je, da je lahko drugi parameter funkcije `addRoute()` klicni ukaz, ki se izvede, ko se odpre ustrezna stran. + + +Zakaj uporabljati Nette za mikrostrani? .[#toc-why-use-nette-for-microsites] +---------------------------------------------------------------------------- + +- Razvijalci, ki so kdaj preizkusili [Tracy |tracy:], si danes ne morejo predstavljati kodiranja brez njega. +- Predvsem pa boste uporabili sistem za oblikovanje predlog [Latte |latte:], saj boste iz samo dveh strani želeli ločiti [postavitev in vsebino |latte:template-inheritance]. +- In vsekakor se boste želeli zanesti na [samodejno pobeganje, |latte:safety-first] da bi preprečili ranljivosti XSS. +- Nette zagotavlja tudi, da se v primeru napake ne bodo nikoli prikazala sporočila o napaki PHP, temveč bo namesto tega prikazana uporabniku prijazna stran. +- Če želite od uporabnikov pridobiti povratne informacije, na primer v obliki kontaktnega obrazca, lahko dodate tudi [obrazce |forms:] in [zbirko podatkov |database:]. +- Izpolnjene obrazce lahko preprosto [pošljete |mail:] tudi po [elektronski pošti |mail:]. +- Včasih se vam lahko zdi koristno [predpomnjenje |caching:], na primer pri prenašanju in prikazovanju virov. + +V današnjem času, ko sta hitrost in učinkovitost ključnega pomena, je pomembno imeti orodja, ki vam omogočajo doseganje rezultatov brez nepotrebnih zamud. Okvir Nette ponuja prav to - hiter razvoj, varnost in široko paleto orodij, kot sta Tracy in Latte, ki poenostavljajo postopek. Samo namestite nekaj paketov Nette in gradnja takšne mikrostrani bo postala enostavna. Poleg tega veste, da ni nobenih skritih varnostnih pomanjkljivosti. diff --git a/best-practices/sl/pagination.texy b/best-practices/sl/pagination.texy index b5322e1cd1..6a86384b63 100644 --- a/best-practices/sl/pagination.texy +++ b/best-practices/sl/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository V Presenter nato injiciramo razred model in v metodi render bomo zahtevali objavljene članke, ki jih posredujemo predlogi: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -V predlogi bomo poskrbeli za upodobitev seznama člankov: +Predloga `default.latte` bo nato poskrbela za seznam člankov: ```latte {block content} @@ -114,7 +113,7 @@ Naslednji korak je urejanje predstavnika. Številko trenutno prikazane strani bo Metodo upodabljanja razširimo tudi na pridobitev primerka Paginatorja, njegovo nastavitev in izbiro pravilnih člankov za prikaz v predlogi. HomePresenter bo videti takole: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ Predloga že iterira po člankih na eni strani, dodajte le povezave za paging: ``` -To je način, kako smo s Paginatorjem dodali paginacijo. Če namesto [Nette |database:explorer] [Database Core |database:core] kot plast podatkovne baze uporabimo [Nette Database Explorer |database:explorer], lahko izvedemo paging tudi brez Paginatorja. Razred `Nette\Database\Table\Selection` vsebuje metodo [page |api:Nette\Database\Table\Selection::_ page] z logiko paginacije, ki je prevzeta iz Paginatorja. +To je način, kako smo s Paginatorjem dodali paginacijo. Če namesto [Nette |database:explorer] [Database Core |database:sql-way] kot plast podatkovne baze uporabimo [Nette Database Explorer |database:explorer], lahko izvedemo paging tudi brez Paginatorja. Razred `Nette\Database\Table\Selection` vsebuje metodo [page |api:Nette\Database\Table\Selection::_ page] z logiko paginacije, ki je prevzeta iz Paginatorja. Skladišče bo videti takole: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository Namesto tega bomo uporabili metodo predmeta `Selection`, ki ga je vrnil repozitorij: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/sl/post-links.texy b/best-practices/sl/post-links.texy new file mode 100644 index 0000000000..8c588b5864 --- /dev/null +++ b/best-practices/sl/post-links.texy @@ -0,0 +1,59 @@ +Kako pravilno uporabljati povezave POST +*************************************** + +V spletnih aplikacijah, zlasti v upravnih vmesnikih, bi moralo veljati osnovno pravilo, da se dejanja, ki spreminjajo stanje strežnika, ne smejo izvajati z metodo HTTP GET. Kot pove že ime metode, naj se GET uporablja samo za pridobivanje podatkov in ne za njihovo spreminjanje. +Za dejanja, kot je brisanje zapisov, je primerneje uporabiti metodo POST. Čeprav bi bilo idealno uporabiti metodo DELETE, je brez JavaScripta ni mogoče izvesti, zato se v preteklosti uporablja metoda POST. + +Kako to storiti v praksi? Uporabite ta preprost trik. Na začetku predloge ustvarite pomožni obrazec z identifikatorjem `postForm`, ki ga boste nato uporabili za gumbe za brisanje: + +```latte .{file:@layout.latte} +
    +``` + +S tem obrazcem lahko uporabite `
    @@ -36,10 +38,12 @@ Formlar Ortak ----- - [Yapılandırma dosyası nasıl yüklenir |bootstrap:] +- [Mikro siteler nasıl yazılır |microsites] - [Nette neden PascalCase sabit gösterimini kullanıyor? |https://blog.nette.org/tr/kodda-daha-az-ciglik-atmak-icin] - [Nette neden Interface son ekini kullanmıyor? |https://blog.nette.org/tr/oenek-ve-sonekler-arayuez-adlarina-ait-degildir] - [Composer kullanım ipuçları |composer] - [Editörler ve araçlar hakkında ipuçları |editors-and-tools] +- [Nesne yönelimli programlamaya giriş |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/tr/attribute-requires.texy b/best-practices/tr/attribute-requires.texy new file mode 100644 index 0000000000..41ce8c21e6 --- /dev/null +++ b/best-practices/tr/attribute-requires.texy @@ -0,0 +1,179 @@ +Nasıl Kullanılır `#[Requires]` Öznitelik +**************************************** + +.[perex] +Bir web uygulaması yazarken, uygulamanızın belirli bölümlerine erişimi kısıtlama ihtiyacıyla sık sık karşılaşırsınız. Belki de bazı isteklerin yalnızca bir form aracılığıyla veri gönderebilmesini (dolayısıyla POST yöntemini kullanarak) veya yalnızca AJAX çağrıları tarafından erişilebilir olmasını istiyorsunuzdur. Nette Framework 3.2'de, bu tür kısıtlamaları zarif ve açık bir şekilde ayarlamanıza olanak tanıyan yeni bir araç sunulmuştur: `#[Requires]` nitelik. + +Nitelik, PHP'de bir sınıf veya yöntemin tanımından önce eklediğiniz özel bir işarettir. Esasen bir sınıf olduğu için, aşağıdaki örneklerin çalışması için use cümlesini eklemeniz gerekir: + +```php +use Nette\Application\Attributes\Requires; +``` + +Kullanabilirsiniz `#[Requires]` özniteliğinin sunum yapan sınıfın kendisiyle ve bu yöntemlerle olan ilişkisi: + +- `action()` +- `render()` +- `handle()` +- `createComponent()` + +Son iki yöntem de bileşenlerle ilgilidir, bu nedenle özniteliği onlarla da kullanabilirsiniz. + +Öznitelik tarafından belirtilen koşullar karşılanmazsa, bir HTTP 4xx hatası tetiklenir. + + +HTTP Yöntemleri .[#toc-http-methods] +------------------------------------ + +Erişim için hangi HTTP yöntemlerine (GET, POST, vb.) izin verileceğini belirtebilirsiniz. Örneğin, yalnızca bir form göndererek erişime izin vermek istiyorsanız, şunu ayarlayın: + +```php +class AdminPresenter extends Nette\Application\UI\Presenter +{ + #[Requires(methods: 'POST')] + public function actionDelete(int $id): void + { + } +} +``` + +Durum değiştirme eylemleri için neden GET yerine POST kullanmalısınız ve bunu nasıl yapmalısınız? [Kılavuzu okuyun |post-links]. + +Bir yöntem veya bir dizi yöntem belirtebilirsiniz. Özel bir durum, sunum yapanların [güvenlik nedeniyle |application:presenters#http-method-check] varsayılan olarak izin vermediği tüm yöntemleri etkinleştirmek için `'*'` değeridir. + + +AJAX Çağrıları .[#toc-ajax-calls] +--------------------------------- + +Bir sunucunun veya yöntemin yalnızca AJAX istekleri için erişilebilir olmasını istiyorsanız, şunu kullanın: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +Aynı Köken .[#toc-same-origin] +------------------------------ + +Güvenliği artırmak için, isteğin aynı etki alanından yapılmasını zorunlu tutabilirsiniz. Bu, [CSRF |nette:vulnerability-protection#cross-site-request-forgery-csrf]'ye karşı güvenlik açığını önler: + +```php +#[Requires(sameOrigin: true)] +class SecurePresenter extends Nette\Application\UI\Presenter +{ +} +``` + +İçin `handle()` yöntemlerinde, aynı etki alanından erişim otomatik olarak gereklidir. Bu nedenle, herhangi bir etki alanından erişime izin vermek istiyorsanız, şunu belirtin: + +```php +#[Requires(sameOrigin: false)] +public function handleList(): void +{ +} +``` + + +Forward üzerinden erişim .[#toc-access-via-forward] +--------------------------------------------------- + +Bazen bir sunucuya erişimi kısıtlamak yararlı olabilir, böylece yalnızca dolaylı olarak, örneğin başka bir sunucudan `forward()` veya `switch()` yöntemleri kullanılarak kullanılabilir. Hata sunucuları, bir URL'den tetiklenmelerini önlemek için bu şekilde korunur: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Pratikte, yalnızca sunum yapan kişideki mantığa dayalı olarak erişilebilen belirli görünümleri işaretlemek genellikle gereklidir. Yine, böylece doğrudan açılamazlar: + +```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 + { + } +} +``` + + +Spesifik Eylemler .[#toc-specific-actions] +------------------------------------------ + +Ayrıca, bir bileşen oluşturmak gibi belirli kodların yalnızca sunum aracındaki belirli eylemler için erişilebilir olmasını da kısıtlayabilirsiniz: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +Tek bir eylem için bir dizi yazmaya gerek yoktur: `#[Requires(actions: 'default')]` + + +Özel Nitelikler .[#toc-custom-attributes] +----------------------------------------- + +Eğer kullanmak istiyorsanız `#[Requires]` özniteliğini aynı ayarlarla tekrar tekrar kullanmak istemiyorsanız, kendi özniteliğinizi oluşturabilirsiniz. `#[Requires]` ve ihtiyaçlarınıza göre ayarlayın. + +Örneğin, `#[SingleAction]` yalnızca `default` eylemi aracılığıyla erişime izin verir: + +```php +#[Attribute] +class SingleAction extends Nette\Application\Attributes\Requires +{ + public function __construct() + { + parent::__construct(actions: 'default'); + } +} + +#[SingleAction] +class SingleActionPresenter extends Nette\Application\UI\Presenter +{ +} +``` + +Ya da `#[RestMethods]` REST API için kullanılan tüm HTTP yöntemleri aracılığıyla erişime izin verecektir: + +```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 +{ +} +``` + + +Sonuç .[#toc-conclusion] +------------------------ + +Bu `#[Requires]` özelliği, web sayfalarınıza nasıl erişileceği konusunda size büyük esneklik ve kontrol sağlar. Basit ama güçlü kurallar kullanarak uygulamanızın güvenliğini ve düzgün çalışmasını artırabilirsiniz. Gördüğünüz gibi, Nette öznitelikleri kullanmak sadece işinizi basitleştirmekle kalmaz, aynı zamanda güvenli hale de getirir. + +{{sitename: Best Practices}} diff --git a/best-practices/tr/composer.texy b/best-practices/tr/composer.texy index 74d2c032f1..f16faee810 100644 --- a/best-practices/tr/composer.texy +++ b/best-practices/tr/composer.texy @@ -142,6 +142,12 @@ Veya doğrudan `composer.json` dosyasında: ``` +PHP Sürümünü Yoksayma .[#toc-ignoring-php-version] +================================================== + +Paketler genellikle hem uyumlu oldukları en düşük PHP sürümünü hem de test edildikleri en yüksek sürümü belirtirler. PHP'nin daha yeni bir sürümünü kullanmayı planlıyorsanız, belki de test amacıyla, Composer böyle bir paketi yüklemeyi reddedecektir. Çözüm, Composer'ın gerekli PHP sürümünün üst sınırlarını göz ardı etmesine neden olan `--ignore-platform-req=php+` seçeneğini kullanmaktır. + + Yanlış Raporlar .[#toc-false-reports] ===================================== @@ -183,7 +189,7 @@ Ancak, Composer'ı `vendor` klasörü dışındaki diğer sınıfları yüklemek Daha sonra, her değişiklikte `composer dumpautoload` komutunu çalıştırmak ve otomatik yükleme tablolarının yeniden oluşturulmasına izin vermek gerekir. Bu son derece zahmetlidir ve bu görevi, aynı etkinliği arka planda otomatik olarak ve çok daha hızlı gerçekleştiren [RobotLoader'a |robot-loader:] emanet etmek çok daha iyidir. -İkinci seçenek [PSR-4'ü |https://www.php-fig.org/psr/psr-4/] takip etmektir. Basitçe söylemek gerekirse, ad alanlarının ve sınıf adlarının dizin yapısına ve dosya adlarına karşılık geldiği bir sistemdir, yani `App\Router\RouterFactory`, `/path/to/App/Router/RouterFactory.php` dosyasında bulunur. Yapılandırma örneği: +İkinci seçenek [PSR-4'ü |https://www.php-fig.org/psr/psr-4/] takip etmektir. Basitçe söylemek gerekirse, ad alanlarının ve sınıf adlarının dizin yapısına ve dosya adlarına karşılık geldiği bir sistemdir, yani `App\Core\RouterFactory`, `/path/to/App/Core/RouterFactory.php` dosyasında bulunur. Yapılandırma örneği: ```js { diff --git a/best-practices/tr/dynamic-snippets.texy b/best-practices/tr/dynamic-snippets.texy index bae43f06d3..30980e895e 100644 --- a/best-practices/tr/dynamic-snippets.texy +++ b/best-practices/tr/dynamic-snippets.texy @@ -35,7 +35,7 @@ public function handleUnlike(int $articleId): void Ajaxlaştırma .[#toc-ajaxization] ================================ -Şimdi bu basit uygulamaya AJAX'ı getirelim. Bir makalenin derecelendirmesini değiştirmek, yönlendirmeli bir HTTP isteği gerektirecek kadar önemli değildir, bu nedenle ideal olarak arka planda AJAX ile yapılmalıdır. AJAX bağlantılarının `ajax` CSS sınıfına sahip olduğu olağan kuralıyla [eklentilerdeki işleyici |https://componette.org/vojtech-dobes/nette.ajax.js/] komut dosyasını kullanacağız. +Şimdi bu basit uygulamaya AJAX'ı getirelim. Bir makalenin derecelendirmesini değiştirmek, yönlendirmeli bir HTTP isteği gerektirecek kadar önemli değildir, bu nedenle ideal olarak arka planda AJAX ile yapılmalıdır. AJAX bağlantılarının `ajax` CSS sınıfına sahip olduğu olağan kuralıyla [eklentilerdeki işleyici |application:ajax#toc-naja] komut dosyasını kullanacağız. Ancak, bunu özellikle nasıl yapmalı? Nette 2 yol sunuyor: dinamik snippet yolu ve bileşen yolu. Her ikisinin de artıları ve eksileri var, bu yüzden bunları tek tek göstereceğiz. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ Elbette görünüm şablonunu değiştireceğiz ve sunucuya bir fabrika eklememi ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/tr/form-reuse.texy b/best-practices/tr/form-reuse.texy index 338dee09ef..f6a3cd40a4 100644 --- a/best-practices/tr/form-reuse.texy +++ b/best-practices/tr/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` - `FormFactory` ve `EditFormFactory` sınıfları arasındaki bağın nesne kalıtımı ile değil, bileşim ile uygulanması çok önemlidir: +`FormFactory` ve `EditFormFactory` sınıfları arasındaki bağın [nesne kalıtımı |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance] ile değil, [bileşim |nette:introduction-to-object-oriented-programming#composition] ile uygulanması çok önemlidir: ```php // ⛔ HAYIR! MİRAS BURAYA AİT DEĞİL @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory Bu durumda kalıtım kullanmak tamamen ters etki yaratacaktır. Çok hızlı bir şekilde sorunlarla karşılaşırsınız. Örneğin, `create()` yöntemine parametre eklemek isterseniz; PHP, imzasının ebeveynden farklı olduğuna dair bir hata bildirecektir. Ya da `EditFormFactory` sınıfına yapıcı aracılığıyla bir bağımlılık aktarırken. Bu, yapıcı [cehennemi |dependency-injection:passing-dependencies#Constructor hell] dediğimiz şeye neden olur. -Genel olarak, kalıtım yerine bileşimi tercih etmek daha iyidir. +Genellikle [kalıtım yerine bileşimi |dependency-injection:faq#Why composition is preferred over inheritance] tercih etmek daha iyidir. Form İşleme .[#toc-form-handling] @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/tr/lets-create-contact-form.texy b/best-practices/tr/lets-create-contact-form.texy index 52d307b8bf..306be16c72 100644 --- a/best-practices/tr/lets-create-contact-form.texy +++ b/best-practices/tr/lets-create-contact-form.texy @@ -39,7 +39,7 @@ Gördüğünüz gibi iki metot oluşturduk. İlk yöntem `createComponentContact Peki ya kullanıcı bazı alanları doldurmazsa? Bu durumda, ona bunun gerekli bir alan olduğunu bildirmeliyiz. Bunu `setRequired()` metodu ile yaptık. Son olarak, form başarıyla gönderildiğinde tetiklenen bir `onSuccess`[olayı |nette:glossary#events] da ekledik. Bizim durumumuzda, gönderilen formun işlenmesiyle ilgilenen `contactFormSucceeded` yöntemini çağırır. Bunu birazdan koda ekleyeceğiz. - `contantForm` bileşeninin `templates/Home/default.latte` şablonunda oluşturulmasına izin verin: + `contantForm` bileşeninin `Home/default.latte` şablonunda oluşturulmasına izin verin: ```latte {block content} diff --git a/best-practices/tr/microsites.texy b/best-practices/tr/microsites.texy new file mode 100644 index 0000000000..977df38b40 --- /dev/null +++ b/best-practices/tr/microsites.texy @@ -0,0 +1,63 @@ +Mikro Siteler Nasıl Yazılır +*************************** + +Şirketinizin yaklaşan bir etkinliği için hızlı bir şekilde küçük bir web sitesi oluşturmanız gerektiğini düşünün. Basit, hızlı ve gereksiz komplikasyonlardan uzak olmalı. Bu kadar küçük bir projenin sağlam bir framework gerektirmediğini düşünebilirsiniz. Peki ya Nette çerçevesini kullanmak bu süreci önemli ölçüde basitleştirip hızlandırabilirse? + +Basit web siteleri oluştururken bile konfordan vazgeçmek istemezsiniz. Tekerleği yeniden icat etmek istemezsiniz. Tembel olmaktan ve kendinizi şımartmaktan çekinmeyin. Nette Framework mikro framework olarak da mükemmel bir şekilde kullanılabilir. + +Böyle bir mikro site neye benzeyebilir? Örneğin, web sitesinin tüm kodu genel klasördeki tek bir `index.php` dosyasına yerleştirilebilir: + +```php +<?php + +require __DIR__ . '/../vendor/autoload.php'; + +$configurator = new Nette\Bootstrap\Configurator; +$configurator->enableTracy(__DIR__ . '/../log'); +$configurator->setTempDirectory(__DIR__ . '/../temp'); + +// config.neon dosyasındaki yapılandırmaya göre bir DI konteyneri oluşturun +$configurator->addConfig(__DIR__ . '/../app/config.neon'); +$container = $configurator->createContainer(); + +// Yönlendirmeyi ayarlayın +$router = new Nette\Application\Routers\RouteList; +$container->addService('router', $router); + +// URL için rota https://example.com/ +$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { + // tarayıcı dilini algılar ve /en veya /de vb. URL'ye yönlendirir. + $supportedLangs = ['en', 'de', 'cs']; + $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); + $presenter->redirectUrl("/$lang"); +}); + +// URL için rota https://example.com/cs nebo https://example.com/en +$router->addRoute('', function ($presenter, string $lang) { + // uygun şablonu görüntüleyin, örneğin ../templates/en.latte + $template = $presenter->createTemplate() + ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); + return $template; +}); + +// uygulamayı çalıştırın! +$container->getByType(Nette\Application\Application::class)->run(); +``` + +Diğer her şey üst `/templates` klasöründe depolanan şablonlar olacaktır. + + `index.php` adresindeki PHP kodu önce [ortamı kur |bootstrap:]ar, sonra [rotaları |application:routing#dynamic-routing-with-callbacks] tanımlar ve son olarak uygulamayı çalıştırır. Bunun avantajı, `addRoute()` işlevinin ikinci parametresinin, ilgili sayfa açıldığında çalıştırılan bir çağrılabilir olmasıdır. + + +Mikro Siteler için Neden Nette Kullanılmalı? .[#toc-why-use-nette-for-microsites] +--------------------------------------------------------------------------------- + +- [Tracy |tracy:] 'yi denemiş olan geliştiriciler bugün onsuz kodlama yapmayı hayal bile edemezler. +- Ancak her şeyden önce, [Latte |latte:] şablonlama sistemini kullanacaksınız, çünkü sadece 2 sayfadan [düzen ve içeriği |latte:template-inheritance] ayırmak isteyeceksiniz. +- Ve XSS güvenlik açıklarını önlemek için kesinlikle [otomatik kaçışa |latte:safety-first] güvenmek isteyeceksiniz. +- Nette ayrıca bir hata durumunda PHP hata mesajlarının asla görüntülenmemesini, bunun yerine kullanıcı dostu bir sayfanın görüntülenmesini sağlar. +- Kullanıcılardan geri bildirim almak istiyorsanız, örneğin bir iletişim formu şeklinde, [formlar |forms:] ve [veritabanı |database:] da ekleyebilirsiniz. +- Doldurulan formların [e-posta ile gönderilmesini |mail:] de kolayca sağlayabilirsiniz. +- Bazen, örneğin beslemeleri indirirken ve görüntülerken [önbelleğe |caching:] almayı yararlı bulabilirsiniz. + +Hız ve verimliliğin kilit önem taşıdığı günümüzde, gereksiz gecikmeler olmadan sonuçlara ulaşmanızı sağlayan araçlara sahip olmak önemlidir. Nette framework tam da bunu sunuyor - hızlı geliştirme, güvenlik ve süreci basitleştiren Tracy ve Latte gibi çok çeşitli araçlar. Sadece birkaç Nette paketi kurun ve böyle bir mikro site oluşturmak çocuk oyuncağı haline gelsin. Ve hiçbir gizli güvenlik açığı olmadığını bilirsiniz. diff --git a/best-practices/tr/pagination.texy b/best-practices/tr/pagination.texy index 5ae4641731..2498d2dd12 100644 --- a/best-practices/tr/pagination.texy +++ b/best-practices/tr/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository Presenter'da daha sonra model sınıfını enjekte edeceğiz ve render yönteminde şablona aktardığımız yayınlanmış makaleleri isteyeceğiz: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -Şablonda, bir makale listesi oluşturmaya özen göstereceğiz: +`default.latte` şablonu daha sonra makaleleri listelemekle ilgilenecektir: ```latte {block content} @@ -114,7 +113,7 @@ Bir sonraki adım sunucuyu düzenlemektir. Şu anda görüntülenen sayfanın nu Ayrıca Paginator örneğini almak, ayarlamak ve şablonda görüntülenecek doğru makaleleri seçmek için render yöntemini genişletiyoruz. HomePresenter şu şekilde görünecektir: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ class HomePresenter extends Nette\Application\UI\Presenter ``` -Paginator kullanarak sayfalamayı bu şekilde ekledik. Veritabanı katmanı olarak Nette Database [Core |database:core] yerine Nette [Database Explorer |database:explorer] kullanılırsa, Paginator olmadan da sayfalama uygulayabiliriz. `Nette\Database\Table\Selection` sınıfı, Paginator'dan alınan sayfalama mantığı ile [sayfa |api:Nette\Database\Table\Selection::_ page] yöntemini içerir. +Paginator kullanarak sayfalamayı bu şekilde ekledik. Veritabanı katmanı olarak Nette Database [Core |database:sql-way] yerine Nette [Database Explorer |database:explorer] kullanılırsa, Paginator olmadan da sayfalama uygulayabiliriz. `Nette\Database\Table\Selection` sınıfı, Paginator'dan alınan sayfalama mantığı ile [sayfa |api:Nette\Database\Table\Selection::_ page] yöntemini içerir. Depo şu şekilde görünecektir: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository Presenter'da Paginator oluşturmak zorunda değiliz, bunun yerine repository tarafından döndürülen `Selection` nesnesinin yöntemini kullanacağız: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/tr/post-links.texy b/best-practices/tr/post-links.texy new file mode 100644 index 0000000000..94bef4ceec --- /dev/null +++ b/best-practices/tr/post-links.texy @@ -0,0 +1,59 @@ +POST Bağlantıları Nasıl Doğru Kullanılır? +***************************************** + +Web uygulamalarında, özellikle de yönetim arayüzlerinde, sunucunun durumunu değiştiren eylemlerin HTTP GET yöntemi ile gerçekleştirilmemesi temel bir kural olmalıdır. Metot adından da anlaşılacağı üzere GET sadece veri almak için kullanılmalıdır, değiştirmek için değil. +Kayıt silme gibi eylemler için POST yönteminin kullanılması daha uygundur. İdeal olan DELETE yöntemini kullanmak olsa da, bu JavaScript olmadan çağrılamaz, bu nedenle POST tarihsel olarak kullanılır. + +Pratikte nasıl yapılır? Bu basit numarayı kullanın. Şablonunuzun başında, daha sonra silme düğmeleri için kullanacağınız `postForm` tanımlayıcısına sahip bir yardımcı form oluşturun: + +```latte .{file:@layout.latte} +
    +``` + +Bu form ile, bir `
    @@ -36,10 +38,12 @@ Спільне ------- - [Як завантажити конфігураційний файл |bootstrap:] +- [Як писати мікросайти |microsites] - [Чому Nette використовує нотацію констант у регістрі PascalCase? |https://blog.nette.org/uk/sob-mense-kriku-v-kodi] - [Чому Nette не використовує суфікс Interface? |https://blog.nette.org/uk/prefiksi-ta-sufiksi-ne-potribni-v-nazvah-interfejsiv] - [Поради щодо використання Composer |composer] - [Поради щодо редакторів та інструментів |editors-and-tools] +- [Вступ до об'єктно-орієнтованого програмування |nette:introduction-to-object-oriented-programming]
    diff --git a/best-practices/uk/attribute-requires.texy b/best-practices/uk/attribute-requires.texy new file mode 100644 index 0000000000..543ae066c2 --- /dev/null +++ b/best-practices/uk/attribute-requires.texy @@ -0,0 +1,179 @@ +Як використовувати атрибут `#[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 .[#toc-http-methods] +-------------------------------- + +Ви можете вказати, які 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-method-check]. + + +AJAX-дзвінки .[#toc-ajax-calls] +------------------------------- + +Якщо ви хочете, щоб доповідач або метод був доступний лише для AJAX-запитів, використовуйте цей параметр: + +```php +#[Requires(ajax: true)] +class AjaxPresenter extends Nette\Application\UI\Presenter +{ +} +``` + + +Одного походження .[#toc-same-origin] +------------------------------------- + +Щоб підвищити безпеку, ви можете вимагати, щоб запит був зроблений з того ж домену. Це запобігає [вразливості до 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 +{ +} +``` + + +Доступ через Форвард .[#toc-access-via-forward] +----------------------------------------------- + +Іноді корисно обмежити доступ до презентера так, щоб він був доступний лише опосередковано, наприклад, за допомогою методів `forward()` або `switch()` з іншого презентера. Так захищаються презентери, що спричиняють помилки, щоб запобігти їхньому запуску з URL-адреси: + +```php +#[Requires(forward: true)] +class ForwardedPresenter extends Nette\Application\UI\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 + { + } +} +``` + + +Конкретні дії .[#toc-specific-actions] +-------------------------------------- + +Ви також можете обмежити доступ до певного коду, наприклад, створення компонента, лише для певних дій у презентері: + +```php +class EditDeletePresenter extends Nette\Application\UI\Presenter +{ + #[Requires(actions: ['add', 'edit'])] + public function createComponentPostForm() + { + } +} +``` + +Для однієї дії не потрібно писати масив: `#[Requires(actions: 'default')]` + + +Користувацькі атрибути .[#toc-custom-attributes] +------------------------------------------------ + +Якщо ви хочете використовувати атрибут `#[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 +{ +} +``` + + +Висновок .[#toc-conclusion] +--------------------------- + +Атрибут `#[Requires]` надає вам велику гнучкість і контроль над тим, як здійснюється доступ до ваших веб-сторінок. Використовуючи прості, але потужні правила, ви можете підвищити безпеку та належне функціонування вашого додатку. Як бачите, використання атрибутів у Nette може не тільки спростити вашу роботу, але й убезпечити її. + +{{sitename: Best Practices}} diff --git a/best-practices/uk/composer.texy b/best-practices/uk/composer.texy index 2eead46d70..e6777c8700 100644 --- a/best-practices/uk/composer.texy +++ b/best-practices/uk/composer.texy @@ -142,6 +142,12 @@ composer require php 8.2.3 --no-update ``` +Ігнорування версії PHP .[#toc-ignoring-php-version] +=================================================== + +Пакунки зазвичай вказують як найнижчу версію PHP, з якою вони сумісні, так і найвищу версію, з якою вони були протестовані. Якщо ви плануєте використовувати ще новішу версію PHP, можливо, з метою тестування, Composer відмовиться встановлювати такий пакунок. Вирішенням проблеми є використання параметра `--ignore-platform-req=php+`, який змушує Composer ігнорувати верхні межі необхідної версії PHP. + + Неправдиві повідомлення .[#toc-false-reports] ============================================= @@ -183,7 +189,7 @@ Packagist.org - глобальний репозиторій .[#toc-packagist-org Згодом необхідно виконувати команду `composer dumpautoload` при кожній зміні та дозволяти таблицям автозавантаження регенеруватися. Це вкрай незручно, і набагато краще довірити цю задачу [RobotLoader |robot-loader:], який виконує ту ж саму роботу автоматично у фоновому режимі і набагато швидше. -Другий варіант - слідувати [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Простіше кажучи, це система, в якій простори імен та імена класів відповідають структурі каталогів та іменам файлів, тобто `App\Router\RouterFactory` знаходиться у файлі `/path/to/App/Router/RouterFactory.php`. Приклад конфігурації: +Другий варіант - слідувати [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Простіше кажучи, це система, в якій простори імен та імена класів відповідають структурі каталогів та іменам файлів, тобто `App\Core\RouterFactory` знаходиться у файлі `/path/to/App/Core/RouterFactory.php`. Приклад конфігурації: ```js { diff --git a/best-practices/uk/dynamic-snippets.texy b/best-practices/uk/dynamic-snippets.texy index 40867a88cf..33872a3b0e 100644 --- a/best-practices/uk/dynamic-snippets.texy +++ b/best-practices/uk/dynamic-snippets.texy @@ -35,7 +35,7 @@ Template: Аяксизація .[#toc-ajaxization] ============================== -Тепер давайте привнесемо AJAX у цей простий додаток. Зміна рейтингу статті не настільки важлива, щоб вимагати HTTP-запит із переспрямуванням, тому в ідеалі це має бути зроблено за допомогою AJAX у фоновому режимі. Ми будемо використовувати [скрипт обробника з додат |https://componette.org/vojtech-dobes/nette.ajax.js/] ків зі звичайною угодою, що AJAX-посилання мають CSS клас `ajax`. +Тепер давайте привнесемо AJAX у цей простий додаток. Зміна рейтингу статті не настільки важлива, щоб вимагати HTTP-запит із переспрямуванням, тому в ідеалі це має бути зроблено за допомогою AJAX у фоновому режимі. Ми будемо використовувати [скрипт обробника з додат |application:ajax#toc-naja] ків зі звичайною угодою, що AJAX-посилання мають CSS клас `ajax`. Однак як це зробити конкретно? Nette пропонує 2 способи: спосіб динамічних фрагментів і спосіб компонентів. Обидва мають свої плюси та мінуси, тому ми покажемо їх по черзі. @@ -92,7 +92,7 @@ public function handleLike(int $articleId): void if ($this->isAjax()) { // ... $this->template->articles = [ - $this->connection->table('articles')->get($articleId), + $this->db->table('articles')->get($articleId), ]; } else { // ... @@ -101,7 +101,7 @@ public function handleLike(int $articleId): void public function renderDefault(): void { if (!isset($this->template->articles)) { - $this->template->articles = $this->connection->table('articles'); + $this->template->articles = $this->db->table('articles'); } } ``` @@ -151,7 +151,7 @@ class LikeControl extends Nette\Application\UI\Control ```php protected function createComponentLikeControl() { - $articles = $this->connection->table('articles'); + $articles = $this->db->table('articles'); return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { return new LikeControl($articles[$articleId]); }); diff --git a/best-practices/uk/form-reuse.texy b/best-practices/uk/form-reuse.texy index 7993290a51..38d700e84f 100644 --- a/best-practices/uk/form-reuse.texy +++ b/best-practices/uk/form-reuse.texy @@ -152,7 +152,7 @@ class EditFormFactory } ``` -Дуже важливо, щоб зв'язок між класами `FormFactory` і `EditFormFactory` був реалізований за допомогою композиції, а не успадкування об'єктів: +Дуже важливо, що зв'язування між класами `FormFactory` та `EditFormFactory` реалізовано [за допомогою композиції |nette:introduction-to-object-oriented-programming#composition], а не [успадкування об'єктів |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance]: ```php // НІ! СПАДЩИНА ТУТ НЕ МАЄ ЗНАЧЕННЯ @@ -172,7 +172,7 @@ class EditFormFactory extends FormFactory Використання успадкування в цьому випадку було б абсолютно контрпродуктивним. Ви б дуже швидко зіткнулися з проблемами. Наприклад, якщо ви захочете додати параметри до методу `create()`; PHP повідомить про помилку, що його сигнатура відрізняється від батьківської. Або при передачі залежності класу `EditFormFactory` через конструктор. Це призведе до того, що ми називаємо пеклом [конструктора |dependency-injection:passing-dependencies#Constructor hell]. -Загалом, краще надавати перевагу композиції, а не успадкуванню. +Як правило, краще віддавати перевагу [композиції, а не успадкуванню |dependency-injection:faq#Why composition is preferred over inheritance]. Обробка форм .[#toc-form-handling] @@ -335,7 +335,7 @@ class MyPresenter extends Nette\Application\UI\Presenter ) { } - protected function createComponentEditForm(): Form + protected function createComponentEditForm(): EditControl { $control = $this->controlFactory->create(); diff --git a/best-practices/uk/lets-create-contact-form.texy b/best-practices/uk/lets-create-contact-form.texy index 724f0c58d7..e4cc290b82 100644 --- a/best-practices/uk/lets-create-contact-form.texy +++ b/best-practices/uk/lets-create-contact-form.texy @@ -39,7 +39,7 @@ class HomePresenter extends Presenter Але що, якщо користувач не заповнить деякі поля? У такому випадку ми повинні повідомити йому, що це поле є обов'язковим для заповнення. Ми зробили це за допомогою методу `setRequired()`. Нарешті, ми також додали [подію |nette:glossary#events] `onSuccess`, яка спрацьовує в разі успішного відправлення форми. У нашому випадку вона викликає метод `contactFormSucceeded`, який відповідає за обробку надісланої форми. Ми додамо його до коду за мить. -Нехай компонент `contantForm` рендериться в шаблоні `templates/Home/default.latte`: +Нехай компонент `contantForm` рендериться в шаблоні `Home/default.latte`: ```latte {block content} diff --git a/best-practices/uk/microsites.texy b/best-practices/uk/microsites.texy new file mode 100644 index 0000000000..05599232db --- /dev/null +++ b/best-practices/uk/microsites.texy @@ -0,0 +1,63 @@ +Як писати мікросайти +******************** + +Уявіть, що вам потрібно швидко створити невеликий сайт для майбутньої події вашої компанії. Він повинен бути простим, швидким і без зайвих складнощів. Ви можете подумати, що такий невеликий проект не потребує надійного фреймворку. Але що, якщо використання фреймворку 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 nebo https://example.com/en +$router->addRoute('', 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#dynamic-routing-with-callbacks] і, нарешті, запускає додаток. Перевага полягає в тому, що другим параметром функції `addRoute()` може бути виклик, який виконується при відкритті відповідної сторінки. + + +Чому варто використовувати Nette для мікросайтів? .[#toc-why-use-nette-for-microsites] +-------------------------------------------------------------------------------------- + +- Розробники, які хоч раз спробували [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 index b0883298b5..17b96a8aa6 100644 --- a/best-practices/uk/pagination.texy +++ b/best-practices/uk/pagination.texy @@ -11,7 +11,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -34,7 +33,7 @@ class ArticleRepository Потім у презентері ми вводимо клас моделі і в методі `render` запитуємо опубліковані статті, які передаємо в шаблон: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -53,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -У шаблоні ми подбаємо про виведення списку статей: +Після цього шаблон `default.latte` подбає про список статей: ```latte {block content} @@ -114,7 +113,7 @@ class ArticleRepository Ми також розширюємо метод `render` для отримання екземпляра Paginator, його налаштування та вибору потрібних статей для відображення в шаблоні. HomePresenter матиме такий вигляд: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; @@ -181,7 +180,7 @@ class HomePresenter extends Nette\Application\UI\Presenter ``` -Ось як ми додали пагінацію за допомогою Paginator. Якщо замість [Nette Database Explorer |database:explorer] як шар бази даних використовується [Nette Database Core |database:core], ми можемо реалізувати підкачку навіть без Paginator. Клас `Nette\Database\Table\Selection` містить метод [page |api:Nette\Database\Table\Selection::_ page] з логікою пагінації, взятою з Paginator. +Ось як ми додали пагінацію за допомогою Paginator. Якщо замість [Nette Database Explorer |database:explorer] як шар бази даних використовується [Nette Database Core |database:sql-way], ми можемо реалізувати підкачку навіть без Paginator. Клас `Nette\Database\Table\Selection` містить метод [page |api:Nette\Database\Table\Selection::_ page] з логікою пагінації, взятою з Paginator. Репозиторій матиме такий вигляд: @@ -190,7 +189,6 @@ namespace App\Model; use Nette; - class ArticleRepository { public function __construct( @@ -198,7 +196,6 @@ class ArticleRepository ) { } - public function findPublishedArticles(): Nette\Database\Table\Selection { return $this->database->table('articles') @@ -211,7 +208,7 @@ class ArticleRepository Нам не потрібно створювати Paginator у презентері, натомість ми використовуватимемо метод об'єкта `Selection`, який повертає сховище: ```php -namespace App\Presenters; +namespace App\Presentation\Home; use Nette; use App\Model\ArticleRepository; diff --git a/best-practices/uk/post-links.texy b/best-practices/uk/post-links.texy new file mode 100644 index 0000000000..b0e7213fc5 --- /dev/null +++ b/best-practices/uk/post-links.texy @@ -0,0 +1,59 @@ +Як правильно використовувати POST-посилання +******************************************* + +У веб-додатках, особливо в адміністративних інтерфейсах, основним правилом має бути те, що дії, які змінюють стан сервера, не повинні виконуватися за допомогою методу HTTP GET. Як випливає з назви методу, GET слід використовувати тільки для отримання даних, а не для їх зміни. +Для таких дій, як видалення записів, доцільніше використовувати метод POST. Хоча ідеальним варіантом було б використання методу DELETE, але його неможливо викликати без JavaScript, тому історично використовується POST. + +Як це зробити на практиці? Використовуйте цей простий трюк. На початку вашого шаблону створіть допоміжну форму з ідентифікатором `postForm`, яку ви потім будете використовувати для кнопок видалення: + +```latte .{file:@layout.latte} +
    +``` + +У цій формі ви можете використовувати `